OPENCLAW MACOS
STALE_
LAUNCHAGENT_
TOKEN.
2026.4.x 周期里,一类「我明明在控制面/doctor 里轮换或升级后拿到了新 Gateway Token,但 Telegram、企业 IM、Web UI、CLI 统一报 401 / unauthorized;而本机偶尔还能 `openclaw gateway status` 出绿」的故障,根源往往在 macOS LaunchAgent:上一条 `openclaw gateway install` 把 OPENCLAW_GATEWAY_TOKEN 写死进 plist,之后你只更新了 shell 里的导出或某份 compose 环境块,launchctl 实际拉起的那份仍是旧值。更糟的是:不加 --force 时 gateway install 可能提示「已安装」而不改写 plist,让你误以为对齐完成。本文给生产可审计的 plist → 环境 → 进程 对齐 Runbook、远程 Mac 的 SSH 检查表,以及令牌变更时的三条硬门禁。可与《升级 breaking 与环境鉴权》《Docker/CLI 与 token compose》《换机迁移与 Gateway 重配》《launchd/systemd 常驻》交叉阅读。
1. 为什么「全端 401」仍可能看到局部状态正常?
1)消费者比 Gateway 更敏感:频道插件、浏览器 UI、部分 RPC 客户端会把 401 明晃晃打在日志里;而某些本地探针只检查进程与端口,并不验证 Authorization 头与当前 token 是否一致。2)双真源:交互式终端里你 export 了新 token,但 launchd 子进程继承的是 plist 里的 EnvironmentVariables——两边不一致时,「我明明改过了」其实只是改了 A 份环境。3)轮换窗口:若控制面已使旧 token 失效,而常驻 Gateway 仍持旧值,则任何依赖 Gateway 中转的路径都会集体 401,直到 plist 与进程一致为止。4)误报「已安装」:openclaw gateway install 在检测到既有 Agent 时可能跳过覆盖;若你期望它刷新 token,必须显式走强制或手动编辑 plist 的路径。
2. 症状—证据矩阵(先对照再动刀)
| 现象 | 优先怀疑 | 首选证据 |
|---|---|---|
| 所有频道与 UI 同时 401 | Gateway 侧 Authorization 与当前 token 不一致 | 抓一条失败请求的响应体与 Gateway 日志中的 auth 关键字 |
| 仅 launchd 拉起的实例失败,手动前台启动正常 | plist 内嵌旧 OPENCLAW_GATEWAY_TOKEN | plutil -p ~/Library/LaunchAgents/*.openclaw*.plist | grep -i token(路径按实际 Label) |
| 执行 install 仍提示已安装、症状不变 | 未触发 plist 覆盖 | 对比 install 前后 plist mtime;必要时 --force |
| 远程 headless Mac 必现 | SSH 会话与 GUI 用户 launchd 域不一致 | 确认 plist 所在用户、launchctl print gui/$uid 与登录会话 |
3. 根因解剖:LaunchAgent 如何「放大」一次遗漏的 token 更新
macOS 将 LaunchAgent 视为声明式配置:一旦 OPENCLAW_GATEWAY_TOKEN 被写入 plist,后续 unless bootout/load 或重写文件,子进程不会自动「跟着你的 zshrc 变」。这与 Linux systemd 的 Environment= 片段同理,只是排查工具换成了 launchctl 与 plutil。生产上建议把「令牌轮换」当作一次小型发布:更新密钥材料 → 更新 plist/compose → 滚动重启 → 用同一条探针请求验证 200 与业务收发,而不是只盯着「进程还在」。
4. 七步 Runbook(本机 + 可写进工单)
Step 01:冻结与标注版本
记录 OpenClaw CLI/Gateway 版本、plist Label、以及轮换发生的时间点;避免同事并行执行第二次 install。
Step 02:找出真实 plist 路径
常见位置为 ~/Library/LaunchAgents/ 下以 openclaw 或自定义 Label 命名的文件;若曾多用户安装,核对是哪一个 $HOME。
Step 03:比对三份 token 视图
分别采集:A)plist 中 OPENCLAW_GATEWAY_TOKEN;B)当前交互 shell(若有);C)控制面/密钥管理系统中的「当前有效值」前两段哈希或指纹切勿把完整 secret 贴进工单。三者不一致即停止猜测,进入改写 plist。
Step 04:优雅卸下再改写入
对对应 Label 执行 launchctl bootout gui/$UID/…(路径以官方文档为准),编辑 plist 替换 token 或删除内嵌键改由外部统一注入(见 Step 07),再 bootstrap/load。
Step 05:需要刷新 install 时使用 --force
若你选择用 CLI 重写 plist而非手剪 XML,执行带强制标志的 openclaw gateway install --force(具体子命令以你安装版本帮助为准),并在前后对比文件校验和。
Step 06:分层验证
顺序建议:Gateway 日志出现 healthy → 单频道最小消息回环 → 再开 Web UI。若仍 401,回看是否还有第二份 plist、Docker 侧独立 env、或 《silent Gateway 诊断》里提到的 doctor 路径被跳过。
Step 07:安全收敛(强烈建议)
长期应将 token 放在受限文件或密钥环,由 plist 仅引用路径或 launchctl setenv 在受控脚本中设置;避免明文长期躺在 world-readable 备份里。轮换后作废旧备份或加密归档。
5. 远程 Mac:SSH 十条检查项
1)确认登录的是装 Gateway 的同一 Unix 用户。2)echo $HOME 与 plist 路径一致。3)launchctl print gui/$(id -u) 中对应服务是否存在。4)plist 的 ProgramArguments 是否指向预期二进制(避免多版本 nvm 路径漂移)。5)是否在迁移后仍引用旧 workspace。6)防火墙仅放行必要端口。7)区分 Aqua 会话与 ssh-only:某些场景需用户图形登录后 Agent 才加载。8)日志目录权限。9)与《Docker token》混部时核查 compose 与 launchd 的优先级。10)收尾时在工单贴验证截图与已脱敏日志行而非完整 secret。
Hard gates
门禁 A:轮换后 10 分钟内 plist mtime 必须变化或与工单备注的强制 install 记录一致。门禁 B:至少一条非探针业务请求返回 200。门禁 C:旧 token 在密钥系统中标记 revoked,防止回滚误用。
6. 误区复盘
「我只改了 .env」但 Gateway 由 launchd 拉起 → 仍 401。「install 成功打印绿」但未覆盖 plist → 仍 401。「本机 curl localhost 200」但客户端走域名与 TLS 另一套路由 → 要分开验证。把这类事故写进团队 runbook,比事后 grep 全机房更快。
7. 延伸:与升级/迁移的交界
breaking 升级常同时改 device identity 与 Gateway 校验策略;若你在 doctor 后只更新了其中一半环境,就会出现「部分 RPC 过、部分频道死」的夹生状态。《升级 breaking》里的环境自检命令应与本 Runbook 的 plist 段同一工单串联执行。换机场景则先保证 《迁移》 完成后只做一次「单一真源」的 token 下发,避免新旧机器各持一半。
8. 事故时间线模板:轮换后 0–30 分钟「作战室」怎么说才不掉坑
T+0~2 分钟:冻结。口令只有两句:「禁止二次 install 撞车」「禁止没看 plist 就全员重登」。把当前 Gateway 版本、控制面轮换工单号、以及**怀疑中的 LaunchAgent Label**贴进置顶频道。T+3~8 分钟:证据。各采集一条带 request_id 的 401 响应与 Gateway 侧 auth 日志行;若只有客户端日志没有网关日志,大概率是打到另一台反代或旧端口。T+9~15 分钟:真源对齐。三人并行:A 读 plist 指纹,B 读 Docker compose env(若存在),C 读 vault 当前版本;任何两方不一致直接判定为配置漂移而非「控制面挂了」。T+16~25 分钟:改写与滚动。bootout→改 plist 或 force install→bootstrap;滚动顺序建议「先停消费者再启 Gateway」还是「先启 Gateway 再放开消费者」写进你们自己的 SOP,但全程只允许一种顺序。T+26~30 分钟:门禁关闭。没有通过前文 Hard gates 之前,禁止宣布 all-clear。
这一段的价值在于:401 类事故最怕「口头对齐」。把时间轴与责任动作钉死,能把平均恢复时间从「群聊吵架两小时」压到「照表执行半小时」量级——尤其在远程 Mac 上,当事人往往不在机房,文字化 SOP 就是唯一可靠信道。
9. 双实例、端口与反代:你以为连的是 A,其实打到 B
较常见的分支是:工程师在调试时手动前台起了一份 Gateway 占用了 127.0.0.1:18789(示例端口),而 launchd 那份仍在后台跑另一端口;控制面下发的新 token 只同步到了其中一条链路。另一个分支是 NGINX/Caddy 把 /gateway 反代到旧上游,TLS 证书更新了新,但 proxy_pass 仍指向去年备份机。处理策略:为每个环境定义唯一 health 路径,并在轮换后强制 curl 两条:loopback 与公网域名各一次;两者返回的 build 哈希须一致。若只有 loopback 成功,优先查反代与 DNS,而不是继续 rotate token。
Docker Desktop 与 Colima 混用时期,还可能出现「你以为容器读的是 host 的 plist env,其实 compose 里写死了旧值」。此时 launchctl 与 docker compose 构成第三真源;我们的建议是:要么把 Gateway 只放在一种载体(仅 launchd 或仅 compose),要么在变更 RFC 里强制 dual-write 检查表,禁止只改一边。
10. 工程师打印版:plist ↔ launchctl ↔ 进程 三角证据链
| 层级 | 你要看到的「健康信号」 | 典型坏掉长什么样 |
|---|---|---|
| plist 文件 | mtime 在轮换窗口内变化;plutil 无语法错误 | 文本编辑器留下未闭合 tag;或权限变 0644 以外导致 launchd 静默拒绝 |
| launchctl | launchctl print gui/$uid/… 中程序路径与 which 一致 | 指向已删除的 nvm Node;或 ExitTimeout 异常飙高 |
| 进程环境 | 对 pid 取 env 可见 OPENCLAW_GATEWAY_TOKEN 与 vault 指纹一致 | env 缺键但仍能启动——往往是读了别的配置文件,排查更阴间 |
| 日志 | auth 拒绝与客户端 401 同一分钟收敛 | 日志静默但 CPU 高——先排除 jsonl/bootstrap 类阻塞,再回到令牌主线 |
macOS 与 Linux systemd 的类比只有一半成立:systemd 常在 unit 里 EnvironmentFile=-;launchd 则习惯把键直接塞进 plist。不要照搬你在 Ubuntu VPS 上的 muscle memory,而是用本文表格强行建立「每层必须有一条证据」的习惯,才能把远程 headless Mac 上的差分一次打穿。
11. FAQ:轮换后仍 401 的「隐蔽分支」
问:我明明在 plist 里换了 token,为什么还是 401?答:确认你改的是被 launchctl bootstrap 的那份,而不是 Desktop 同步盘里的备份副本;建议用 launchctl print 里的 plist path 反查。问:我可以只 revoke 旧 token 不重启吗?答:不推荐。旧进程仍可能在若干秒内持有内存中的字符串;生产上应用「先滚动进程再 revoke」或极短暂重叠窗口,窗口长度由你的威胁模型决定。问:为什么 doctor 全绿仍失败?答:doctor 验证的路径可能短于完整 channel handshake;请以真实频道消息回环为准。问:多用户 Mac mini 怎么隔离?答:每个 Unix 用户各用各的 ~/Library/LaunchAgents,不要让 root 与 staff 混用同一份 workspace。
问:我想把 token 从 plist 挪到钥匙串,值不值得?答:值得中长期做,但要在测试机先验证 launchd 可读性;不要周五晚高峰直接上。**问:轮换后要不要顺带轮换 SSH/API 密钥?**答:若同一事件里出现过泄露怀疑,应一次性处理;否则避免「啥都旋一遍」导致变更面爆炸。
12. 结论文案
macOS 上 OpenClaw Gateway 的 401 「全员阵亡」多数是令牌真源分裂而非神秘网络问题:LaunchAgent plist 是其中最隐蔽、也最需要强制刷新的一环。把 plist 纳入变更清单,轮换就与发版同级严肃。你在找不吃灰的 Mac mini 云端算力做常驻 Gateway 时,可先看 MACGPU 定价 与 Telegram 群(群内关键词:MACGPU)。若需为团队定制「token + launchd + Docker」单一流水线,也可通过站点入口联系。本文由 MACGPU 团队撰写,旨在压缩排障时间,配置以官方 CLI 帮助为准。