① 两个身份,两堵不同的墙
| Gode(核心型) | GodeMini01(沙盒型) | |
|---|---|---|
| 对标 | Vega 那几家 | LivaMini01 |
| 系统身份 | jeffchoong | hoopai |
| 工作目录 | HOOP 主工作区 | /Users/hoopai/games |
| 墙靠什么立 | WIP 认领 / 热区 / Git 纪律(约定) | 系统用户权限(内核级) —— 不靠自觉 |
| 读主仓 | ✅ 可以 | ❌ Permission denied |
| 生产服务器 / Git 远程钥匙 | 有 | ❌ 一把都没有 |
两家的 HOOP 账号、私聊、专属 thread、状态文件、日志、账号级 flock 全部独立, 不与任何 Claude 身份共用。
② 调用链:墙长在启动器里,哨兵一字不改
launchd
└─ ws_sentinel.py ──软链──▶ codex_ws_sentinel_generic.py ← ① fail-closed 入口(新增)
└─只读 exec──▶ ws_sentinel_generic.py ← 共享哨兵,零改动
└─ claude_bin ──▶ codex_adapter_generic.sh ← ② 适配器(新增)
├─ codex_bridge.py fetch ← ③ 把 Jeff 的话拉进提示词
├─ codex_bin ──▶ codex (Gode:本机)
│ └▶ codex_via_hoopai.sh ──ssh──▶ hoopai 的 codex
└─ codex_bridge.py reply ← ④ 把回答发进 HOOP + 回读
为什么共享哨兵一个字都不用改:ws_sentinel_generic.py:55-64 早就有
「按家覆盖启动器」的机制,注释原话是「墙就长在启动器里,哨兵其余逻辑一字不改」。
Codex 这条线整个挂在这个已有的口子上。
为什么入口是"薄壳"而不是把哨兵拷一份
2026-07-31 真栽过:五家各存一份拷贝,Atlas 改了 generic,而分发早于他的改动 —— 五家拿到的全是旧版,而且没有任何一处报错。
整份 fork = 1453 行的手工同步对照表(铁律 23)。所以这份入口只加一道闸、不复制任何逻辑: 检查通过后把共享哨兵原样读进来执行。共享哨兵将来怎么改,这边自动跟上,没有"分发"这一步可以漏。
可行的前提是查过的:共享哨兵的 DIR 从 argv[1] 算(第 47 行),
不从 __file__ 算;全文 __file__ 只用在第 1159 行做 mtime 戳。
③ 四处静默降级 —— 每一处都"看起来成功了"
这一节是这条线最贵的部分。四处全部由真实运行暴露,不是设想出来的。
| # | 症状 | 为什么危险 | 怎么堵的 |
|---|---|---|---|
| 1 | 执行器路径坏 → 哨兵静默退回默认 claude |
Claude 会顶着 Gode 的身份干活,唯一痕迹是启动日志一行 | fail-closed 入口:七项检查任一不过 → rc=78,launchctl 显示 pid=- |
| 2 | codex_bin 坏或被整个删掉 → 退回本机 Codex |
GodeMini01 会以 jeffchoong 身份跑 —— 整堵沙盒墙被绕过 |
取消默认回退,人人显式声明;八种破坏全 rc=78 |
| 3 | codex rc=0 但一句话都没产出 |
哨兵按成功推进记位 → 消息被吃掉、不重试,人只看到"它不理我" | 没有 agent_message 一律按失败,rc=65 |
| 4 | 模型说"已发",其实一条没到 | 同上,而且更难查 —— 它自己也以为发了 | 发送归外层;POST + 回读都成功才 rc=0,否则 rc=66 |
共同的判据:退出码好看不算数。 「这一轮到底有没有产出一句给人看的话?有没有真的到达对面?」——答不上来就是失败。
④ 传话桥:读和发都归外层,模型只管答
哨兵拼给分身的提示词是 PREAMBLE + task.md,里面没有消息正文 ——
它只敲门,不把信递进去。Claude 那几家靠自己去拉(<名字>_lib);
一开始照这条路教 Codex 自己拉,那是错的:
- 靠模型"记得去拉" = 随机。它醒来看到"必须回复",就回一句「收到」交差 —— 真发生了三轮。
- 每轮重新 OTP 登录 = 撞后端 429 限流。
- 发送由模型临时写 Python = 引号/换行随时把内容吃掉,而且没人能验证它到底发没发。
判据(和 recheck_order() 同一条):
写进前言 = 指望它读到、想起来、愿意做 = 随机;
由程序拼进提示词 = 它第一眼就看见 = 一致。
fetch 读 ws_state.json 的游标(此刻还是上一轮的)→ 拉比它新的消息
→ 结构化区块注入:cid · 消息 id · 发件人 · 时间 · 正文
→ 语音/图片不给占位符:带类型 + 可取地址,并明写"必须先转文字"
reply 把最终回答 POST 进 HOOP → sleep → 回读核对 → 找到了才 exit 0
开关沿用「存在即承诺」语义:~/.hoop/<名字>/codex_reply_bridge 写 on ——
一旦开着,拉不到或发不出都算本轮失败,绝不静默降级。
⑤ 单一出处表(哪个值住哪儿)
这条线一路上栽在"抄第二份"上不止一次,所以把出处钉在这里。
| 值 | 唯一出处 | 怎么核实"实际用的是它" |
|---|---|---|
| 模型 | ~/.hoop/<名字>/model |
grep 'exec:' ws.err 看真实 argv 的 -m |
| 执行器 | ~/.hoop/<名字>/codex_bin | 同上,看第一段路径 |
| 沙盒档 / 网络 | codex_sandbox / codex_network |
同上,看 -s 与 network_access |
| 会话 | ~/.hoop/<名字>/session.id | 首轮由适配器写入,之后不许手改 |
| 守哪条会话 | plist 的第 4 个参数 | launchctl print 一眼看得见 |
2026-08-16 真栽过:task.md里抄了一句「执行器是 Codex(gpt-5.6-terra)」。 Jeff 把 Gode 换成sol后,那份抄本没跟着变 —— Gode 照着它告诉 Jeff 错的模型,又不得不自己发一条更正。 改法不是把 terra 改成 sol(那还是抄),是删掉,并教它: 「问你用什么模型,如实说『运行时没暴露给我,我不猜』」。
⑥ macOS TCC —— 一个和代码无关、但会让一切失效的坑
~/Desktop 受 macOS 隐私保护,launchd 起的任务一律读不到。而主仓就在那儿。
launchd → /bin/bash ls 仓库 → Operation not permitted launchd → /usr/bin/python3 ls 仓库 → Operation not permitted launchd → claude 二进制 读仓库 → ✅ 成功(它早就被授过权) launchd → codex 二进制 读仓库 → ❌ 被拒 ← 差别在这儿
TCC 按可执行文件授权,不随父进程继承。所以修法不是改代码,是
把 codex 那个二进制加进「完全磁盘访问权限」(只能人在系统设置里点)。
授权后实测:Gode 读回 CLAUDE.md 首行,与真值一字不差。
连错两版的判据:守卫里判断"这个目录能不能用",
第一版用 [ -r ](TCC 不体现在权限位上,access 说可读、open 仍被拒);
第二版改成"真的 ls 一次"——仍然错,因为量的是 bash 的能力,而授权是给 codex 的。
定案:这里只需要一个能站进去的目录;能不能读那里的文件,是 codex 自己的事。
⑦ 验收证据(不是"没报错",是真产出)
| 项 | 证据 |
|---|---|
| 端到端唤醒 | 门铃 23:53:07 → 叫醒 23:53:11 → 完成 23:53:16 rc=0。
Jeff 问「1+2等于多少」,Gode 答 3;问 GodeMini01「5+6=多少」,答 5+6=11 |
| 权限边界 | 同一次自检:Gode「工作目录是主仓,可以读」;GodeMini01「工作目录 /Users/hoopai/games,不能读主仓」 |
| 跨进程恢复 | 两条独立 SSH 连接;第二轮开跑前核过 hoopai 下存活 codex 进程 = 0。暗号一字不差答回 |
| 沙盒未被绕过 | 八种破坏 codex_bin,jeffchoong 侧 Codex 会话数 53 → 53,一条都没多 |
| Claude 侧未受影响 | 八家 pid 全程未变;共享脚本 md5 b5bc7fbb… 未变 |
| 两个兜底周期无自锁 | 22:41:38 → 22:52:18,零自锁记录,锁空闲,心跳连续 |
⑦½ 点名与讣告 —— 08-16 补的两层(Jeff 亲自定的判据)
Gode 上线当天暴露两件事,都不是"参数没调对",是判据放错了地方。
一、点名:谁被 @ 是发送时的事实,不是收件时的猜测
症状:Vega 在正文里写一句 `@Gode` —— 在讲这个名字,不是在叫它 —— Gode 照样醒。
第一反应是在收件侧加规则(跳过反引号、跳过引用行…),Jeff 直接把方向掰过来:
「多 AI 房点名必须优先使用消息的结构化 mention UID,禁止仅凭正文、引用、代码块中的@Gode唤醒。」
「如果 HOOP 后端没有结构化 mention 数据,必须先补后端,不能继续用字符串匹配打补丁。」
查下去发现洞在后端,而且形状很值得记:同一个能力有两条通路,只有其中一条漏了字段。
| 通路 | 谁在用 | 带得上 mentions 吗 |
|---|---|---|
WS message.send | App(Flutter) | ✅ 整体反序列化 SendInput,一直好好的 |
POST /v1/conversations/{id}/text | bot / 脚本 / AI 分身唯一的发送口 | ❌ 逐字段拼 SendInput 时漏了 Mentions |
从任何一端看都"功能是有的",只有跨端对照才看得见。而这个洞逼着收件侧退回字符串匹配, 于是必然误伤 —— 实测两种病一起犯:
- 谈论 ≠ 交给:提到名字就叫醒 → 它回一句「我不插话」→ 那句里又有别人的名字 → 连环白醒(十分钟四次)。
- 名字前缀撞车:旧代码取名字前 4 个字母当变体,
godemini01→gode, 于是@Gode把 GodeMini01 也一起叫醒。
后端 sendText 入参补 mentions 并透传(伪造不用担心:filterMentions 只保留真实会话成员)
发送侧 hoop_mentions.py —— @名字→UID **唯一一处**解析,say_generic / codex_bridge 共用
· 代码围栏 ``` / 行内反引号 / 引用行 里的 @ 不算叫人
· 长名字优先,**而且匹配到要把那一段吃掉** —— 只排序不吃没用:
"@GodeMini01" 这串里本来就含 "@Gode"(第一版就这么错的,测试当场抓住)
收件侧 哨兵只认结构化 mentions;正文扫描只留给天然没有 UID 的广播语义(@所有人)
判据(铁律 23 的同一条):一个事实只能有一个出处。 让每个收件人各自去正文里找自己的名字 —— 有几个收件人就有几份猜法,每份都会漏。
二、讣告:「这一轮没干成」和「我挂了」是两回事
「普通rc=65连败只能报『本轮失败、哨兵在线』,不能发讣告。」
「讣告只用于额度墙、认证失效、执行器不可用或守护进程真正退出。」
「失败次数按触发消息 ID 独立累计。」
讣告是要人来救的信号。乱发几次之后,真挂了那一次也没人信了。 而全局失败计数更阴:A 消息失败一次、B 消息失败一次,合起来就到阈值 —— 两条互不相干的偶发被当成"连续故障"诈死。
| 证据 | 判成 | 发什么 |
|---|---|---|
rc=78(执行器 fail-closed) | 真死 | 讣告 |
| 输出含 session/rate limit · quota · 401 · unauthorized · not logged in · command not found | 真死 | |
其余任何 rc≠0 | 本轮失败 | 「⚠️ 本轮处理连续失败,仍在线,消息保留等待重试」(同一条消息只报一次) |
测试有牙,不是摆设:反例 21 条 + 解析 15 条。
把正文匹配放回去 → 2 条当场红;把全局失败计数放回去 → 1 条当场红;恢复后全绿。
生产端到端四条也真打过接口:显式 mentions 存下 ✅ · 伪造非成员 uid 被 filterMentions 挡掉 ✅ ·
正文 @Gode 解析入库 ✅ · 反引号里的 @Gode 不算点名 ✅。
十家哨兵全部换上新代码,重启前后 PID/软链逐家核过。
⑧ 悬着的事
| 待定 | ~/.claude 目录 755,hoopai 能列目录名 ——
内容读不到(.jsonl 是 600)。一行 chmod 700 能关,
但它落在「现有 Claude 文件一律不动」里,等 Jeff 拍板。 |
| 待测 | thread 越长每轮重放越多 —— 累计已到百万级 token(94% 是缓存读)。 需要单独一轮测准单轮真实增量,再决定要不要定期换 thread。 |
| 缺口 | token 用量栏显示「—」。哨兵的统计读 Claude 格式的账本,
Codex 写的是 rollout-*.jsonl。Jeff 已拍板方案 B:现阶段不改共享脚本去迁就。
(Codex 的 total_token_usage 是累计值,将来要做比 Claude 那套还简单:读最后一条即可。) |
⑨ 新增文件清单
~/.hoop/codex_ws_sentinel_generic.py fail-closed 入口(两家共用,薄壳)
~/.hoop/codex_adapter_generic.sh 适配器(参数翻译 · 沙盒 · argv 落日志 · 假绿灯闸)
~/.hoop/codex_bridge.py 传话桥(fetch 注入 / reply 发送+回读)
~/.hoop/hoop_mentions.py @名字→UID 唯一一处解析(08-16;say_generic 也用它)
~/.hoop/gode/ claude_bin codex_bin codex_cwd codex_sandbox codex_reply_bridge
model session.id task.md gode_lib.py + 4 个软链
~/.hoop/godemini01/ 同上 + codex_via_hoopai.sh + godemini01_lib.py
~/Library/LaunchAgents/com.hoop.{gode,godemini01}.ws.plist
测试(scratchpad,不随分身部署):适配器主测试 26 条 ·
codex_bin fail-closed 14 条 · 假绿灯反例 10 条。每一条都验过"该红时真会红"。