|
|
914e5b4b08
|
★★ pi 597086ad 抓到我"说了但没做到"(第三次同形): 注释称正向对照与正式扫描都走 _scan_text,实际正式扫描走 strip_comments、_scan_text 自带一份 sed ⇒ **三份去注释实现**;我上一封"自检盖住整条管线"的结论**作废**
★ pi 指出的洞(我复现):
我的注释: "正向对照与正式扫描**都走这一个函数**"
实际: 正式扫描 → `strip_comments`(自带 sed)
自检 → `_scan_text`(**也自带一份 sed**)
⇒ 其实有**三份**去注释实现 ⇒ 只漂移 `strip_comments` 一处时
**正式扫描静音、而自检不响**(自检走的是另一份 sed)
★ 而我上一封正是拿"抽成同一函数"当"已盖住 pi 标的残余"的证据 ⇒ **该结论作废**。
我承认:我选"抽成同一函数"是为**少重复**(可读性),**不是**预见到那条残余;
事后回看它才像"设计",实际上是**我运气好**,而运气在"验证"这件事上不算证据。
★ 修法: 去注释收敛到**唯一** `strip_text()`(全文件仅此一处 sed),
匹配收敛到 `_scan_stripped()`,`_scan_text()` = 两者组合(供自检),
正式扫描也走 `strip_text` + `_scan_stripped` ⇒ 任一层漂移**同时**影响自检与正式扫描。
★ 变异矩阵(七向,全部真跑,每次还原复测 rc=0):
[0] 基线无违规 ⇒ rc=**0**(非恒红)✓
[1] 注入 3 处裸赋值 ⇒ rc=1 ✓
[2] 判"有没有"处 -q→-n ⇒ rc=1 ✓
[3] 取行号处换 -q ⇒ rc=1 ✓
[4] ★共模(正则字面量单点改错) ⇒ rc=1 ✓(由"判据自检失败"判红)
[5] ★pi 的漂移(去注释单点) ⇒ rc=1 ✓(由**防空转**判红: 集合空)
[6] ★匹配实现单点静音 ⇒ rc=1 ✓(由"判据自检失败"判红)
★ 我区分了 [5] 与 [6] **各自由哪个机制**判红,没有笼统说"都红了":
[6] 去注释完好 ⇒ 调用者集合仍在 ⇒ 只有自检能抓 ⇒ **自检响** ✓
[5] 去注释坏了 ⇒ 集合空 ⇒ **防空转**先响(自检未必响)—— 两者都 fail-closed,无假绿
★ 验证: criteria-hygiene **9/9** 绿(并发会话又加了一条)
|
2026-09-25 07:22:03 +08:00 |
|
|
|
e44ae45dd3
|
★★★ pi 指出 fail-closed 的**未写出前提**(两通道须失效不相关)——而我的两通道共享正则字面量与 $body ⇒ 共模时 fail-closed 不触发;补**正向对照**(且走同一函数)
★ pi `3be8a091` 的论点我复现:
我的两条通道共享 2 处: (a) **同一个正则字面量**(两处逐字相同)(b) **同一个 $body**
⇒ fail-closed 条件是 `_had=1 ∧ _cnt=0`(**不一致**)⇒
**单点改动同时哑掉两条 ⇒ 两者"一致"(都沉默)⇒ 条件不成立 ⇒ 恰好不触发**
实测(照 pi 做): 把两处正则字面量一起改成 '^NOMATCH=',而树上**注入着 3 处裸赋值**
⇒ rc=**0**,并打出"裸赋值 **0** 处" —— **与真实情况相反** ✓ 与 pi 一致
★ 记法(收 pi 的,并认为它比"条数≥2"准):
**"两条通道"要成为独立证据,须"不存在单点改动能同时哑掉两条"** ——
条数 ≥ 2 **不是**独立性的判据;要检查两通道共享的**字面量/变量/函数/上游命令**。
即: **fail-closed 挡得住"一条通道坏了",挡不住"两条因同一原因一起坏"**。
★ 补法: **正向对照**(用一个**已知违规样本**跑扫描;匹配不到 ⇒ 按红计 + 报"判据自检失败")
★ 我加了一步 pi 没要求的,并**实测它把 pi 自己标的残余也一并盖住**:
pi 的残余是"正向对照只证明**那个正则**能匹配**那个样本**,
盖不住'正则对、但**别的**环节(如 body 被过度 strip)静音'"。
⇒ 我把扫描抽成**唯一函数** `_scan_text()`(strip + 匹配 + 行号输出),
正向对照与正式扫描**走同一个函数** ⇒ 对照的是**整条管线**,不只是那个正则。
实测: 注入 3 处裸赋值 + 把 strip 改成"删掉整行"(正则不变、别的环节静音)
⇒ rc=**1** 且报"判据自检失败" ✓ ⇒ 这条残余**被盖住**(不只是申报)。
★ 变异矩阵(五向,全部真跑 + 每次还原复测):
[0] 基线无违规 ⇒ rc=**0**(**非恒红**)✓
[1] 注入 3 处裸赋值 ⇒ rc=1 ✓
[2] 判"有没有"处 -q→-n ⇒ rc=1 ✓
[3] 取行号处 -n→-q ⇒ rc=1 ✓
[4] ★共模(正则单点改错) ⇒ rc=1 + 报"判据自检失败" ✓(原版此处 rc=0/报"0 处")
[5] ★深层共模(strip 改为删整行,正则不变)⇒ rc=1 + 自检失败 ✓
★ 另: criteria-hygiene 现 **8/8** 绿(并发会话新增的"非门禁工具发现路径"那条也在内)
|
2026-09-25 07:06:38 +08:00 |
|
|
|
4598095b17
|
fix(drift): 补上负向清单**第②项的守护格** —— pi ⑬″ 指出的真洞(宣称 2 项,只有 1 项有人守)
pi `7ec0044a` 用**同一条量法**(`|R\C|`)打了我自己刚落地的那条 ⑬′:
```
我 b5989a9 的 ⑤b 负向清单宣称 2 项:
① 从脏树构建(含未提交代码) —— **有格**(:1610 modified=true ⇒ 绿 + note 点明)
② 部署后被手工替换/修改(只看内嵌 revision)—— **一格都没有**
grep '手工替换' 全文件 ⇒ 只命中清单自身
⇒ 所以 |R\C| 不是 1;② 是**只被"声明"、没被判据守**的那一项
```
★ 而这正是 ⑬′ 想防的形状的**镜像**: 我写了负向清单(防读者把绿读成"没问题"),
却**没有东西防我自己把清单写漂** —— 删掉一项、或以为它被覆盖了,都不会红。
## ⑬″ 的判法(pi 给的,我采纳)
```
每一项判一次: 「在 R 内 C 外」(真洞 ⇒ **加格**)还是「在 R 外」(⇒ **划出宣称**)
项②: ⑤b 宣称回答"这份二进制是不是当前代码" ⇒ **在 R 内**
而 C = {只读内嵌 revision},同 revision 换内容照样绿 ⇒ **C 外**
⇒ **真洞 ⇒ 加格**(不是划出宣称)
```
## 落地
新增自检格 `★网关二进制:绿时 note 必须写明"①脏树构建②部署后手工替换"两项都不覆盖`,
**不要求改 ok** —— 内容替换确实判不了,只能"说出来";它钉的是**那句话还在**。
★ 变异验证(这格**能失败**,否则等于没加):
```
从 note 里删掉负向项② ⇒ 该格 **失败** ⇒ "判据自检失败 1 项"
```
自检 63 → **64 项**,0 失败。
## 一并记账:§7 那条同类无守(pi 同封指出,本条**未改**)
`redeploy-gateway.sh` §7 那句「已装二进制 = 当前 HEAD(…)—— 不覆盖: …」**全仓只命中它自己**,
而 `redeploy-gateway.sh` 没有 `--self-check` ⇒ **同一动作两处,一处有守一处无守**。
不在这条提交里改(要动部署脚本,且得先想清 §7 该怎么自检)—— 留给下一条,避免又造一次半截更新。
|
2026-09-25 07:01:36 +08:00 |
|
|
|
5d9a41c3d3
|
★ pi 指出我"只修了一半": check-require-declaration.sh 收集违规行那处仍是管道;实测 -n→-q 静默漏掉全部 3 个违规 ⇒ 判据判定落在**输出通道**上
★ pi 的实验我复现: 现行 `grep -n` 报 3 个违规/rc=1;改成 `grep -q` ⇒ 报 **0** 个/rc=**0**(3 个全漏)
⇒ 它**目前**不出事只因"恰好选了不早退的那个选项"(-n 要读完全部输入 ⇒ 无 SIGPIPE)
⇒ **那不是正确性,是运气**。与我上一版在 :78-82 修的是同一条,我却漏了 :102。
★ 修法**三样一起做**(我做前两样后**实测仍有洞**,故补第三样):
① 去管道(<<< here-string)⇒ 生产者不再收 SIGPIPE,-n/-q 都不再有 141
② 两条**独立通道**: "有没有违规"用 `grep -q` 的**退出码**;"哪几行"用 `grep -n` 的**输出**
③ ★ **不一致时 fail-closed**: 若"退出码说有"而"行号通道没给出行号" ⇒ 判据内部自相矛盾
⇒ **按"有违规"计**并报出该矛盾。
实测缺 ③ 会怎样: 只做 ①② 时,把取行号那处改成 `-q` ⇒ 报 0 个、rc=**0**(又是全漏)
⇒ **"计数"与"证据"走同一条通道时,那条通道静音就等于"没有"** ⇒
要么分通道,要么规定**静音=失败侧**(本处取后者)。
★ 验证(全部真跑,含三个变异):
· 基线 rc=0(3 个调用者,裸赋值 0 处)
· [A] 注入 3 处裸赋值 ⇒ rc=1 ✓
· [B] 判"有没有"那处 -q→-n ⇒ rc=1 ✓
· [C] 取行号那处 -n→-q(pi 的变异) ⇒ rc=1 ✓ 且 **fail-closed 报出 3 次** ✓
· 每个变异后还原并复测 rc=0;`git diff --numstat` 仅剩我的文件
(同目录另有并发会话在改 check-deploy-drift.mjs —— 未 add 它的改动)
· criteria-hygiene 7/7 绿、pi 的 env-guard 17/17 绿、bash -n 过;生产未动
|
2026-09-25 07:01:34 +08:00 |
|
|
|
d562f55d78
|
采纳 pi 反提案: 声明动作化 agentmail_require + 新判据 check-require-declaration.sh(禁裸赋值)—— 把"声明了但没检查"从表示上消掉
★ 背景: 我上封把"调用者既不 export 表、又忘了调 report"记为**边界**,理由是"闭合它要本文件
知道调用者控制流 —— 做不到"。pi 指出**前提可以换掉**(我复核采纳):
病根不是"控制流不可知",而是"**声明**(赋值)与**检查**(调用)是两个可分离的动作"。
⇒ 把声明做成**动作**: `agentmail_require <命令…>` = 赋值 + 立刻检查 ⇒ 该状态**不存在**。
★ 改动:
· env-defaults.sh 加 `agentmail_require()`(内部就是赋值 + agentmail_env_check_require)
· 三个调用者由 `AGENTMAIL_REQUIRE="…"` 改为 `agentmail_require …`(各 1 行)
也顺带把"重新导出为环境变量"的写法去掉了 —— 表只在本进程内被 ③b 读,不需要 export
· 新判据 `deploy/check-require-declaration.sh`(755): deploy/ 下的**调用者**不得写裸赋值
· install.sh 接线,走 `CHECK_GATE_RC` **累积**通道(照 check-file-modes.sh 的既有做法:
直接调会在 set -e 下中止,把后面的收尾诊断全吃掉)
★★ 判据自己的两个 bug,都是**实测**抓出来的(不是审出来的):
① 假红: 我第一版按"`deploy/**/*.sh` 全扫"划范围 ⇒ 立刻把 `env-defaults.sh:449`
(`agentmail_require()` 函数体里的 `AGENTMAIL_REQUIRE="$*"` —— **动作自己的实现**)
判成违规。⇒ 改为按**性质**划: "调用者"= **真的 source 了本库**的文件,
减去定义动作的那个库自己("调用者"这个身份由 source 这个动作定义,不由目录猜)。
② ★ 假阴且**不稳定**: `strip_comments "$f" | grep -q …` 在 `set -o pipefail` 下,
`grep -q` 一命中就退出 ⇒ `sed` 收 SIGPIPE(rc=141)⇒ **管道整体 141** ⇒ if 判假 ⇒ 漏文件。
实测同一个脚本两次分别找到 **2** 个与 **1** 个调用者(实际 3 个),
而 rc=141 被 if 静默吞掉 ⇒ 集合**偏少且不稳** ⇒ "裸赋值 0 处"会是**假绿**。
⇒ 改为先收集到变量、再 `grep <<<"$body"`(无管道)。修后重复 10 次**稳定 3 个**。
★ 验证(全部真跑):
· 基线 rc=0(3 个调用者,裸赋值 0 处);重复 10 次稳定 3
· 变异①: 把 redeploy-plugin.sh 改回裸赋值 ⇒ rc=**1** ✓
· 变异②: 注释掉一个调用者的 source(仍剩 2)⇒ rc=0 ✓ 非空转
· 变异③: 三个 source 全注释 ⇒ **集合空** ⇒ rc=**1** ✓(防空转,不是恒绿)
· 每个变异后**还原**并复测 rc=0;`git diff --numstat` 确认只有预期行
· criteria-hygiene **7/7** 绿(新 check-*.sh 已被 install.sh 真调用)
· pi 的 env-guard **17/17** 绿;五个脚本 bash -n 全过;生产未动
★ 残留(判据头已如实写): 盖"字面裸赋值",**不盖间接赋值**(经 eval/read/env 注入)——
补它需 shell 语义分析,超出"一条 grep 型判据"的射程。
|
2026-09-25 06:51:25 +08:00 |
|
|
|
e65f41b31b
|
★ 修本文件自己的大假绿: ③b 的 AGENTMAIL_REQUIRE 检查**只在 source 期跑**,而三个调用者都是**先 source 后赋值** ⇒ 那张表从未被检查过
★ 缺陷(pi b6e24606 实测报回,dsh 独立复核成立):
install.sh source 131 / 赋值 135
redeploy-gateway.sh source 42 / 赋值 46
redeploy-plugin.sh source 52 / 赋值 56
⇒ source 那一刻表是**空的** ⇒ `if [ -n "${AGENTMAIL_REQUIRE:-}" ]` 直接跳过 ⇒ 整张表从未生效
★ dsh 独立 A/B(同一台同一 shell):
AGENTMAIL_REQUIRE=__no_such_cmd . env-defaults.sh ⇒ rc=2(检查本身是好的)
. env-defaults.sh; AGENTMAIL_REQUIRE=__no_such_cmd ⇒ rc=0(**漏过**)✓ 与 pi 一致
★ 这正是本文件头反复记的那件事落在**本文件自己**身上: **判据在,但走不到**
—— 与我 60d59f9 造出一个走不到的判据是同一形状,只是这次是"判据在库文件里、而调用相位错开"
★ 修法: 把检查提成 `agentmail_env_check_require()`,**两处都挂**:
· source 期一次 —— 接"表由**环境**提供"(父进程 export / CI 注入)
· `agentmail_env_report()` 内再一次 —— 接"表由**调用者**赋值"(本仓三个都是这种)
不选"改三个调用者的相位": 要改三处,且下一个新调用者还会犯同样的错。
★ ⚠️ 残留缺口(如实申报,不写成"已修好一切"): 若某调用者**既不 export 表、又忘了调
`agentmail_env_report`**,两次都不跑 ⇒ 仍会漏。闭合它要求本文件知道调用者的控制流,做不到。
★ 验证:
· A/B 三向: 赋值在 source 后+调 report ⇒ rc=2 ✓/赋值在 source 前 ⇒ rc=2 ✓/命令都在 ⇒ rc=0(非恒红)✓
· 变异: 删掉 report 体内那次复检 ⇒ rc=0 **退回漏过** ⇒ 证明那次调用**承重** ✓
· 真调用者端到端: 就地在 redeploy-gateway.sh 表里注入 `__no_such_cmd_zz`
⇒ 实跑 rc=2 且报出该名字 ✓;还原后 `缺少命令` 命中 0 ✓(已确认工作区干净)
· pi 的 env-guard 17/17 绿;四个脚本 bash -n 全过;生产未动
|
2026-09-25 06:42:32 +08:00 |
|
|
|
2393703100
|
feat(deploy): 构建步打印**工作树清单** —— 把不可归因的 modified=true 换成有名字的清单
pi 2026-09-25 提的第三种处置(既不判红、也不只留一个 bool),我采纳。
## 为什么一个 bool 不够
`go build` 在脏树上**照样**写 `vcs.revision=<HEAD>`,只额外记 `vcs.modified=true`。
那个位只说"含未提交代码",**不说是谁的什么** —— 而本仓是**多 agent 并发**:
未提交改动通常**不是部署者的**,是别人的在飞工作。
⇒ 这不是理论,本仓**已经付过代价**(`check-deploy-drift` 判据⑥ 注释里那笔账):
2026-09-14 `pool.mjs` 一行未提交的 `let missingSessionCount = 0;` 被"从脏工作区做的"
快照原样带进生产,而当时判据① 报的是「逐字节一致」。
## 处置:打印清单,不判红
```
say "3. 构建二进制" 之后、go build 之前:
git status --porcelain 非空 ⇒ warn "工作树不干净 —— 下面这份未提交清单会被**编进这次构建**" + 逐行打印
否则 ⇒ ok "工作树干净(构建件不含未提交代码)"
```
★ 位置贴紧构建步:`go build` 读的就是**此刻**的工作树,所以这份清单才对应"进了这份二进制的东西"。
★ 判红与否改变不了的事实:**清单有名字,bool 没有** —— 出事时能直接看出"带进去的是谁的那两个文件",
而不只能回答"当时是脏的"。(本会话同族:一个位 vs 一份清单 = 不可归因 vs 可归因。)
## 同时给 ⑤b 记一条**边界**(避免下一个人把它补错)
`check-deploy-drift` 的 ⑤b **读不到构建时的工作树** —— 它读的是二进制内嵌位,
而它那里的 `git status` 问的是**此刻**的树(两者可能不同:构建完又提交了、别人的改动进来了)。
⇒ 注释里写明:清单归**部署脚本**(那里才是同一时刻),⑤b 只回答"含不含未提交代码",
**别把 ⑤b 补成 `git status` 然后当成构建清单**。
## 验证
`bash -n` 通过;`--dry-run` rc=0 且不落地(只留 0 字节锁),清单在 dry-run 下照常打印(只读);
自检 63 项绿、`判据自检失败` 不出现。
|
2026-09-25 06:38:13 +08:00 |
|
|
|
b5989a94a1
|
feat(deploy): 按 ⑬′ 给 ⑤b 与 §7 的绿补**负向清单**(判据须写出它不覆盖哪几类)
pi 2026-09-25 提的 ⑬′:写"要验 X"时必须写清**动作覆盖到哪**,而"覆盖到哪"要写成
**负向清单**(不覆盖哪些)—— 因为正向范围可以含糊,负向清单必须逐条列,否则等于没写。
他给的量化形式:假绿区 = |R| − |C|(R=宣称范围,C=实际能判红的集合)。
这两个判据原先只在**红**的时候说话,**绿**时读者会把"绿"读成"这份二进制没问题" ——
而它们其实只答了一个问题:"内嵌 revision 是否等于 HEAD"。
⑤b note(绿时): …;本判据**不覆盖**: ①从脏树构建(含未提交代码,只披露不判红)
②部署后被手工替换/修改(只看内嵌 revision)
§7 ok(绿时): 已装二进制 = 当前 HEAD(xxxxxxxx)—— 不覆盖: 从脏树构建 / 部署后手工替换
★ 这条规矩**已被它自己验证过一次**:我 ⑤b 的 R\C 里那一格(`modified=true` 的脏树构建)
**不是我想到的,是 pi 找出来的** —— 我写 ⑤b 时以为 R = C。写出负向清单这件事
本身就会把那格逼出来。
自检新增一格 `★网关二进制:绿时 note 必须写负向清单(不覆盖哪几类)`,62 → **63 项**,
`判据自检失败` 不出现;`bash -n` 通过;`--dry-run` rc=0 且不落地。
|
2026-09-25 06:33:46 +08:00 |
|
|
|
eb5c4aa1b4
|
修 env-defaults.sh 自己的一个假绿: HOME 可用性判据从 [ -w ](权限位)改成**真写探针** —— 与我自己上封那个错同一形状
★ 缺陷(我在认自己 `[ -w ]` 假绿时顺带查出来的,就在我的 scope 里):
196 行 `mkdir -p "$_am_home" && [ -w "$_am_home" ]`
179 行 `... || [ ! -w "$_am_home" ]`
两处都只看**权限位**。uid=0 时对任何 root 属主目录都返真,而**实际拦截可能在另一层**
(本会话每次 shell 都带 `landlock-run: partial enforcement`)。本机实测:
[ -w /root ] ⇒ true / touch /root/x ⇒ Permission denied
[ -w /opt/agentmail ] ⇒ true / touch /opt/agentmail/x ⇒ Permission denied
⇒ 而且 `mkdir -p` 对**已存在**的目录不写任何东西(rc=0),所以它**接不住**这个假绿
—— 文件 177 行原本就写了"mkdir -p 会返回成功,所以必须再单独判",但"那个单独判"选错了层。
★ 后果正撞在本文件存在的理由上(**误指对象**):
HOME 空 + uid=0 ⇒ `_am_home=/root` 被误判可用 ⇒ HOME=/root 放行
⇒ ② 段取 `$HOME/.cache/go-build` 当 GOCACHE ⇒ 那里用的是**真写探针**(297 行)⇒ 失败
⇒ 报出「**GOCACHE** 不可写」,而根因是 **HOME** 不可写
⇒ 实测该路径可复现(`env -u HOME bash -c 'source deploy/lib/env-defaults.sh'`)
★ 修法: 加 `_am_can_write()` —— **真去写一个探针文件**(与 297 行同一办法),
179/196 两处都用它。**必须两处一起改**: 196 的 else 是 `exit 2`(不是 fallback),
只改 196 会让 /root 走到 `exit 2` 而不是改判。
★ 顺带把 WARN 的括注从"(非 root / passwd 里没有可用目录)"改成
"(已实测真写探针;uid=%s)" —— 原括注是**按 uid 猜的原因**,而判据已不按 uid 判,
uid=0 也会走到这里。**不报没验过的原因**。
⚠️ 前半句 `按身份推出来的 HOME=%s 不可用` **一个字都没动** —— 它是
`plugins/pi-mail-bridge/test/env-guard.test.mjs:268` 的 assert.match 锚
(我第一版把它改成了"写不进去",**实测把 pi 的测试弄红了 16/17**,已改回)。
★ 变异测试: 把 `_am_can_write` 改成恒真 ⇒ **被抓住**,且复现的正是上面那条误指
(HOME 空时不再改判 ⇒ 直接报「GOCACHE 不可写」)✓
★ 反空真: 探针对 /tmp、/home/program/agentmail 返真,对 /root、/opt/agentmail、
/nonexistent、空串返假 ⇒ 有区分力(非恒真非恒红)✓
★ 验证: pi 的 env-guard 17/17 绿、criteria-hygiene 7/7 绿、三处 source 者 bash -n 全过、
install.sh/redeploy-*.sh 未改。**生产未动**。
|
2026-09-25 06:33:23 +08:00 |
|
|
|
55158e1666
|
fix(deploy): 补 ⑤b 的**假绿洞** revision==HEAD ≠ "就是那份提交"(pi 指出,我实测复现)
## 洞
树脏时 Go **照样**写 `vcs.revision=<HEAD>`,只在 `vcs.modified` 上留痕 ⇒
只比 revision 会把"**未提交的代码**"当成"就是那份提交"。
实测(/tmp/vm2:HEAD=2931050a,改一行**不提交**再 build):
revision=2931050a == HEAD ⇒ 我 ⑤b 判**绿**
但二进制里是未提交的那行代码 ⇒ **假绿** ✓ pi 对
线上那份正是 `vcs.modified=true` ⇒ 它**不对应任何已提交版本**。
## 修法:说清楚,但**不判红**
`revision == HEAD` 与"不含未提交代码"是**两个量**,不能拿前者冒充后者 ——
但脏树在本仓是**多 agent 并发编辑的常态**,判红就是造一条总在亮的判据。
★ 同一个事实在本仓**已有既定严重度**:`check-deploy-drift` 判据⑥
// ⑥ 工作区干净度 —— **WARN,不参与退出码**。
⇒ 两处都改成 **WARN/note 披露,不改 ok / 不加 CHECK_FAIL**,
与判据⑥ 同严重度 —— 否则同仓同一个量会有两套红绿。
redeploy-gateway.sh §7: modified=true ⇒ warn(不动 CHECK_FAIL)
check-deploy-drift ⑤b: revision 相同但 modified=true ⇒ 绿 + note 点明"含未提交代码"
## 判据自检
新增一格:`revision 相同但 modified=true ⇒ 绿,且 note 点明"含未提交代码"`。
自检 **61 → 62 项**,`判据自检失败` 不出现。
★ 这条是本判据**唯一**的假绿形状,所以它必须有一格专门钉住——
否则下一个人把 note 删了、或把 ok 改成 false,都没有判据会响。
|
2026-09-25 06:31:04 +08:00 |
|
|
|
2c34d6e368
|
fix(deploy): 占位行判据改用**行的年龄**(阈值 1 会把健康并发判成泄漏)+ 后置验证补 binrev==HEAD
两件都来自 pi 2026-09-25 的实测,我逐条复跑确认。
## ① `占位行 <= 1` 是阈值版假红(pi 127746b5,我复现)
`ClaimRelay`(INSERT 占位) 与 `BindRelayMail`(UPDATE 绑定) **不在一个事务里** ⇒
"同时有 2 个请求在飞"的正常形态**就是** 2 个占位行(t1 claim、t2 claim、t1 bind、t2 bind)。
变异(/tmp/pl/t.db:清掉化石 + 插 2 个**合法在飞**占位)
旧判据: PLACE=2 ⇒ rc=**1** [FAIL] 占位行 <= 1 ← 把健康并发判成泄漏
新判据: rc=**0**,打印"最老 = 0 分钟;其中 >10 分钟 = 0"
阈值 1 是"只有 1 个请求在飞的那天"的快照,与它要防的病(泄漏)**不正交**。
⇒ 与泄漏正交的判别量是**行的年龄**(pi 给的):正常占位行只活一个请求的时长,
泄漏行活到**下一次请求之后**。年龄与并发度无关,也与"此刻是否恰好有请求"无关。
阈值 10 分钟:远大于任何单请求(HTTP 有超时),远小于真实泄漏尺度(那行活了 12.7 天)。
断言**上界**而非"==0"—— 0 是"恰好没请求在飞",不是健康的条件。
正反两面都验(只验一面会放过退化):
健康并发 2 行 → rc=0 ✓ 13 天泄漏 + 1 健康行 → rc=**1** ✓
真库 → rc=1(那行确实 18259 分钟 ⇒ **真阳性**,非假红)
并打印年龄 ⇒ 顺带答出"泄漏了多久"(旧判据只说"多了一行")。
## ② 抑制数丢了 keying(pi dd579a8e,我复核确认)
生成器键是 `(ag, prefix(k), r)`(**已含 agent**),但**序列化/打印时把 agent 丢了** ⇒
任何人引用这个数都会以为是"仅根"口径。实测两种 keying 全局差 6 封(61/37 vs 55/43,
6 组**全是跨 agent 合并**)⇒ 名字里不写,下一个人**必重犯**。
文本: `抑制数 [keying=(agent,根) scope=全局]: …`
JSON: `suppress_full_groups_keying_agent_root_scope_global`
★ 同时补 **scope**:本脚本报的是全局;session 侧同 keying 是另一个数(4/18)——
同 keying 两个 scope 两个数,少写任一个都只能靠猜。
★ 只改名字,**数的值不动**(这是防再犯,不是纠错)。
## ③ 后置验证补 `binrev == HEAD`(pi 127746b5 §一,落在脚本而非只落检查器)
§7 那几项(服务 active / `/health` 200 / 无 panic / SSE 重连)**全都与代码版本无关** ⇒
**旧二进制跑这套清单会全绿**。2026-09-25 实证:线上是 09-19 的二进制而 §7 全过。
pi 建议落在 `redeploy-gateway.sh`,我采纳 —— 因为我昨天已把同一件事实现为
`check-deploy-drift` 判据 ⑤b,但**那个检查器没有任何自动入口**(要人手跑);
落在部署脚本里才能"装完当场就判"。
三态与 ⑤b 同口径:相等→绿 / 不等→**红**(就地回滚)/ 取不到任一侧→**不判**且必须说出来
(判红会让 `-buildvcs=false`、老构建、非 git 构建恒红)。
实测逻辑: 线上 e8b260dd vs HEAD ⇒ 红 ✓;/nonexistent/binary ⇒ warn 不判 ✓
## 验证
`bash -n` 两文件通过;`recount-relay-counts.sh --json` 合法(23 键);
`criteria-hygiene.test.mjs` 7/7 通过(改名后仍满足 check-*.sh 命名约定)。
|
2026-09-25 06:27:09 +08:00 |
|
|
|
ba5720573e
|
修我自己的 60d59f9: check-relay-counts.sh **改名为 recount-relay-counts.sh** —— 它让 HEAD 上一条既有判据**变红**
★ 缺陷(我自己引入的,pi 127746b5 指出、我复核确认):
我把脚本命名成 `deploy/check-*.sh`,而本仓那条命名族由
`client/electron/test/criteria-hygiene.test.mjs` 用 readdirSync().filter(/^check-.*\.sh$/)
**强制接线**:凡 check-*.sh 必须被 install.sh / redeploy-*.sh 在可执行位置调用。
⇒ 我的脚本 0 调用者 ⇒ **实测该判据由 7/7 绿变 6/7 红**("这些政策门禁写了却没有任何入口脚本调用")。
⇒ 即: 我一边在信里报"判据在但走不到",一边**自己造了一个走不到的判据**。
★ 但接线是**错的解** —— 它按性质就不是门禁(只读、依赖生产库、断的是库的不变量):
· 接 install.sh ⇒ 读 /opt/agentmail/data/agentmail.db,**新机器没有该库**
⇒ 实测 AGENTMAIL_DB=/nonexistent 时 rc=1 ⇒ **每次全新安装假红**
· 接 redeploy-* ⇒ 库是活的、计数会漂 ⇒ 会去**回滚一次正确的部署**
⇒ 正确修法是**改名**,进入"按需工具"族(prune-* / archive-* / reset-* 同族),
并把这条理由写进文件头,免得下一个人再犯。
★ 顺带暴露那条判据的一格(它的自称 vs 它的机制不是一回事):
自称: 按**性质**分 —— "宣称是政策/门禁的 check-*.sh;不管 check-deploy-drift.mjs 这类按需手动工具"
机制: 按**扩展名**分 —— /^check-.*\.sh$/
⇒ 两个方向都可观察:
误报(FP): 按需工具 + .sh 后缀 ⇒ 被当门禁(**我这次**)
漏报(FN): 真门禁 + .mjs 后缀 ⇒ **逃过检查**(实测 check-plugin-snapshot.mjs 被
redeploy-plugin.sh:218 真实调用,是真门禁,却因 .mjs 不被该判据覆盖)
⇒ 这是 pi 那条⑬"判据的动作与它被许诺的范围不匹配"的又一实例。判据本身未改(不在本次范围)。
★ 同时确认别人的修正: 2f0d77a 把我两条 **假红** 断言修对了(我复核成立):
· `HOMEAGENT == SUBSTR_N - PREFIX_N` ⇒ ⊆: 注入第五族 `pi-failure:` 时差集 +1 而 homeagent 不变,
旧式**假红**(新增命名是正常演进)✓
· `RESIDUE == 2 && UNBOUND == 1` ⇒ 只打印 + 用"未绑定 ⊆ 占位"承接:
清掉那行化石是**正确维护动作**,旧式把正确动作判成失败 ✓
⇒ 我的两条确实是把"恰好如此"当成了不变量。**如实记**: 代价是变异④(rename homeagent 族)
从"被抓"变成"存活" —— 那是"过严"与"漏报"的取舍,我取后者(假红会让人不信判据)。
★ 门禁族数量仍为 3(判据反空真要求 >=3)✓;脚本 rc=0;13 条断言全绿。
★ 边界: 只读查库;**生产未动**;未改那条既有判据(属 client/electron/,非本次范围)。
|
2026-09-25 06:25:21 +08:00 |
|
|
|
3c2b7d1385
|
fix(deploy): 还清欠账 redeploy-script-unguarded-steps —— 四类裸步骤接收退出码
出口是这条欠账**自己写的**到期条件("下一次改 `deploy/` 下任一脚本时"):
当天因修 `--dry-run` 落地写入而动了这个脚本,所以一并还。
## ① 前端同步三步
`rm -rf assets` / `rm -f index.html` / `cp -r dist` 原先调用点不接退出码,
而 `run()` 内部是 `eval`(脚本只有 `set -uo pipefail`,无 `-e`)⇒ 失败既不中断也不上报。
`cp` 那条尤其要紧:前端产物没拷进去 ⇒ go:embed 把**旧界面**打进二进制,而所有单测仍绿
(2026-09-14 踩过,见脚本内那段注释)。三处都改成 `|| { bad …; exit 2; }`。
## ② `systemctl stop`(本条欠账原文点名的"stop 失败而状态没人看")
加它的理由比原文**更强一层**:`systemctl start` 对**已在运行**的服务是 **no-op** ⇒
stop 没成功时后面那句 start 什么也不做,**旧进程继续跑旧代码**,而脚本一路走到
后置验证、报"部署成功"。也就是说,"部署脚本跑过了 ≠ 线上跑的是当前代码"这个第 2 类漂移
会被脚本**自己在内部**造出来 —— 与今天线上那件事(09-19 二进制跑了 6 天)同形,只是成因在脚本内。
## ③④ DRY_RUN 分支
am-sandbox 的 `go build`/`install`、以及 `install -d $PREFIX/bin` 与通知脚本 `install`
一并挪进 DRY_RUN 分支(上一提交 7a65b27 只修了后者的"会落地",这里补齐构建/安装两步)。
## 边界(如实记,避免读成"全脚本已无裸步骤")
只覆盖 `redeploy-gateway.sh`。`redeploy-plugin.sh` / `install.sh` 的同类步骤仍未加守卫 ——
那是另一条欠账 `deploy-interrupt-trap-other-scripts` 的范围。
## 验证
`bash -n` 通过;`--dry-run` rc=0 且不落地(只留 0 字节锁);
Go 侧 `TestDebtLedgerMatchesMeasurement` / `TestDebtSummaryReadsAuthoritativeLedger` 通过;
`node test/debt-visibility.test.mjs` 1/1 通过。
|
2026-09-25 06:22:09 +08:00 |
|
|
|
c6a497f4ed
|
feat(deploy): 漂移检查补 ⑤b —— 比 vcs.revision,接住"二进制落后 HEAD"
## 缺口
`check-deploy-drift.mjs` 判据 ⑤ 只判**构建参数**(有没有带 `-trimpath`),
不判**这份二进制是哪次提交建的**。2026-09-25 实证:
线上 /opt/agentmail/agentmail-gateway vcs.revision=e8b260d(09-19 构建)
仓库 HEAD 已有 6 天的新提交(含两条 relay 幂等键/回滚修复)
⑤ 当日报的是"63 处源码路径"(另一个毛病)⇒ **完全没指出它落后 6 天**
当时是靠人手动跑 `go version -m` 才发现的。而"部署脚本跑过了 ≠ 线上跑的是当前代码"
正是这个文件头自己列的第 2 类漂移 —— 判据却缺了它。
## 实现
Go 默认把 `vcs.revision=<sha>` 以**明文字节**编进二进制,所以直接按字节匹配读出,
不依赖 `go` 命令、不 spawn:与 ⑤ 读同一份 buffer。
⚠️ 三态而非两态:**拿不到 revision(`-buildvcs=false`/老构建/非 git 构建)⇒ 不判**。
写成红会让合法环境恒红,写成绿等于没判 —— 所以只接受"不判",且 note 必须说出来,
否则读者分不清"比过且相同"与"根本没比"。只比相等/不等,**不判谁新谁旧**
(判新旧要 git 图,而二进制里的 sha 未必在本地仓库里)。
## 判据自检
新增 3 条(不等→红 / 相等→绿 / 拿不到→不判且 note 说出来),自检 **58 → 61 项**,
`判据自检失败` 不出现。只钉"不等红"会放过"两侧都读不到时恒绿"的退化。
## 顺带修一处脆锚
原有自检用 `o.find(c => c.name.startsWith('已安装的网关二进制'))` 取 ⑤。我新增的
⑤b 名字以同一串开头 ⇒ `find` 会返回它,⑤ 的断言就可能测到 ⑤b。
已把选择器锚到**完整前缀** `'已安装的网关二进制不含源码路径'`,并把新判据命名为
`网关二进制构建版本…`。("按名字锚定"是对的,但**锚得太短就等于按位置**。)
HEAD 版与新版 `--self-check` 同为 rc=1(来自既有真实漂移:3 个宿主待重部署 +
判据 6),非本次引入。
|
2026-09-25 06:19:38 +08:00 |
|
|
|
7a65b270bb
|
fix(deploy)!: --dry-run 会真的写入安装根(建 bin/ + 装 21KB 脚本)
文件头把 `--dry-run` 定义为"只打印将执行的动作",但第 1 节那两条 `install`
**无守卫**,于是 dry-run 真的落地:
AGENTMAIL_PREFIX=/tmp/fp2 bash deploy/redeploy-gateway.sh --dry-run
⇒ /tmp/fp2/bin/service-failure-notify.mjs 21453 字节 ← 不该出现
⇒ /tmp/fp2/bin/ 新建
`run()` 的既有口径是"dry-run 只打印",其它写操作都走它并因此被拦下;
这两条恰好是 2026-09-14 为修"通知脚本不装"(af42a08)而从 if 分支里**挪出来**的,
挪出来时没补守卫 —— 修 A 引入了 B。
同节 1b(am-sandbox)也一样:dry-run 会**真跑一次 go build + 19 项内核判据**
并 install。一并包进 DRY_RUN 分支。
## 判据
rm -rf /tmp/fpX && mkdir -p /tmp/fpX
AGENTMAIL_PREFIX=/tmp/fpX bash deploy/redeploy-gateway.sh --dry-run
修前:残留 bin/ + bin/service-failure-notify.mjs(21453B) + .deploy.lock
修后:残留 .deploy.lock(0 字节) ← flock 载体,非"落地"
`bash -n` 通过;dry-run rc=0。
⚠️ 这与本仓"部署要一次做对"直接相关:`--dry-run` 是上线前的预览动作,
它若自己会写目标目录,就等于**在预览阶段动了生产**,而输出看起来完全正常。
|
2026-09-25 06:16:03 +08:00 |
|
|
|
2f0d77a3d0
|
fix(deploy): check-relay-counts 两条断言是**假红** —— 把"恰好如此"当成了不变量
新脚本的立意(把反复对账的查询固定成命令、断言只写不变量、瞬时量只打印)完全正确,
但它自己破了其中两条规则。两条都实测复现,且都把**正确动作**判成失败。
## 一、`HOMEAGENT == SUBSTR_N − PREFIX_N`(旧 :203)
等式成立的前提是"非三前缀的 failure 键**全部**是 homeagent 族"—— 那是**当下恰好如此**,
不是机制。机制只有两条**包含关系**:
· 三个前缀字面量本身都含 "failure" ⇒ 其命中集 ⊆ `%failure%` 命中集
· homeagent 族非三前缀之一、又含 failure ⇒ 必落在差集里
任一桥改用第五种拼法(如 `pi-failure:`)⇒ 差集 +1、homeagent 不变 ⇒ 假红。
实测:插入一行 `pi-failure:` 后 子串=99 三前缀=82 homeagent=16,差=17 ⇒ 旧断言红。
## 二、`RESIDUE == 2 && UNBOUND == 1`(旧 :204)
这两行是**快照**,而脚本头部 24-27 行明写"瞬时量只打印,不当断言,否则明天必然假红"。
未绑定那行是"claim 后早退没退键"的化石;**清掉它是正确动作**,
旧断言却让清理后变红。实测:清掉该行后 残留=1/未绑定=0 ⇒ 旧断言红。
⇒ 改为只打印(已在"当下读数"里),另用与清理无关的关系接住:
`未绑定 ⊆ 占位`(未绑定必是占位,两者口径一致)。
## 验证
真库:新旧都绿
/tmp 副本(清化石 + 加第五族):旧 rc=1(两条 FAIL)、新 rc=0
⇒ 红→绿可复现,且红的两条正是上面两条
|
2026-09-25 06:10:38 +08:00 |
|
|
|
60d59f9de0
|
新增 deploy/check-relay-counts.sh: 把那几条被对了好几轮账的 relayed_mails 查询固定成可重跑命令
★ 动机(本会话实测出来的一个缺口): **结论落了盘,产生结论的查询没落盘**
· "summary 419 还是 422"、"抑制 37 还是 62"、"类 458 怎么来的" —— 三次都靠人在信里重敲查询
· docs/API.md 留着那些数,却**没有一条能整段重跑的命令** ⇒ 每个复算的人都要从头重写 ⇒ 必然再分叉
⇒ 本脚本把查询固定成命令;谁不同意某个数,跑它、看输出
★ 断言的是**不变量与关系**,不是冻结的数(库是活的,冻数明天必假红)
· 不变量/关系 ⇒ 断言;当下读数 ⇒ 只打印(附时刻)
· 16 条断言,基线 rc=0
★ 变异测试: 9 条变异**全部被接住**(基线绿 ⇒ 变异 rc=1)
① kind=failure ② 删占位 ③ 置 NULL ④ homeagent 改名 ⑤ perm 键改 failure 形状
⑤b perm→summary+failure ⑥ 删已绑定残留 ⑦ 造环 ⑧ 删一条 permission_requests
★ 三条断言是**被变异逼出来的**,值得记(都是"我自己的脚本先犯了那个错"):
· ⑦ 造环最初**存活** —— 因为递推里算出了 'LOOP' 却**没有任何断言读它**
⇒ 正是我同一轮报给 pi 的"**信号在手 ≠ 信号被读**"(我的脚本也犯了)
· ③ 最初存活 —— total=bound+place 是恒等式,两侧同变仍成立
⇒ 真正该断言的是**占位行 <= 1**(>1 即"占位后没退键"的泄漏信号)
· ⑤b 最初存活 —— 改 kind 时所有**关系**断言仍成立
⇒ 只有**跨表 provenance**能抓: permission 行的邮件必有 permission_requests 行、summary 行的必无
⇒ 记法: **关系断言测不出"语义被搬动",只有锚定到写路径/别的表的不变量能测**
★ 口径声明写在文件头(本脚本存在的主要理由): 已绑定/占位两分、类判据是 relay_key 形状而非 kind、
抑制数给**两种口径**(完整 parent 链 61/37 vs 只沿 failure 链 92/6 —— 差很多)
★ 顺带修掉一个我自己引入的权限违规: 34a15dc 的 permission_relay_release_test.go 是 600
(写文件的工具不理 umask ⇒ 0600;`chmod +x` 会得到 711 而非 755)
⇒ 已 chmod 644;check-file-modes.sh 现在只剩 1 个既有违规(非我的 scope 文件)
★ 边界: 只读打开库(mode=ro);生产一个字节没动;/tmp 变异副本已用后即弃
|
2026-09-25 06:08:59 +08:00 |
|
|
|
da5d9339e2
|
把 PATH 归一化提到文件最前(修我 54d641e 那个"只修一半"的修法),并把 --help 从数行号改成锚定
★ 我上一版(54d641e)修了三条触发带,但**引入了第二个问题**(矩阵实测发现):
空 PATH / 最小 PATH 下: de1b072 rc=2("找不到 git"+人话,**符合**本仓环境词汇表)
in-block rc=**128**("fatal: not in a git directory",**不在**词汇表)
⇒ 因为归一化仍在 `REPO=…dirname…`(line 11)**之后** ⇒ dirname 先失败 ⇒ REPO 退化成 /
⇒ ★ "修一处、坏一处":我把一处**符合约定的人话诊断**换成了**裸退出码**
★ 正确位置 = **文件最前**(env-defaults.sh:124 自己也注明 ④「**必须排在最前**」):
4 变体 × 6 种 PATH 矩阵(真 git worktree,工具齐全但 PATH 无 git):
PATH old(^de1b072) de1b072 in-block(54d641e) final
(空) 1/- 2/- 128/- **0/.githooks**
仅 bash/env 1/- 2/- 128/- **0/.githooks**
/opt/tools 0/.githooks 2/- 0/.githooks **0/.githooks**
/usr/local/sbin 0/.githooks 2/- 0/.githooks **0/.githooks**
/sbin:/usr/sbin 0/.githooks 2/- 0/.githooks **0/.githooks**
/usr/bin 0/.githooks 0/.githooks 0/.githooks **0/.githooks**
⇒ final 六行全绿;连跑两次幂等;`command -v git` 真失败时仍 exit 2 + 人话(负控制保留)
★ 顺带修一处**既存**脆弱点(被本次插入暴露,非本次引入):
`--help` 原为 `sed -n '2,20p'`(**数行号**)⇒ 任何人往文件头插行都会让它开始印**实现代码**
(原版就已经把 `set -euo pipefail`/`REPO=…` 印出来;插 PATH 块后会多印 5 行 case…esac)
改为 `usage()` = `sed -n '2,/^set -euo pipefail/p' | sed '$d'`(**锚定**)
实测: --help 7 行纯注释 ✓(原 19 行含代码);相对/绝对/符号链接三种调用都 rc=0 ✓
★ 验证: bash -n OK;mode=755;mode gate 对 install.sh 零命中
★ 边界: 只改 deploy/install.sh 一个文件;生产未动(仍 09-19 13:04)
|
2026-09-25 05:43:57 +08:00 |
|
|
|
54d641e1c5
|
修我 de1b072 引入的活回归(pi 017c0239 报回:--git-hooks 假红"找不到 git"):块内补回 env-defaults ④ 的 PATH 归一化
★ pi 报的回归**实测成立**(我对**自己的**提交做严格 A/B,真 git worktree、工具齐全但 PATH 无 git):
de1b072^(旧位置): rc=0 hooksPath=.githooks ← source 的 ④ 已把 /usr/bin 补回
de1b072 (新位置): rc=2 hooksPath=(未设) ← ④ 还没跑 ⇒ 假红"找不到 git" **且真的没装上**
触发带逐条复现(pi 报的三条全部命中):
PATH=/usr/local/sbin 旧 rc=0 装上 / 新 rc=2 未装
PATH=/opt/tools 旧 rc=0 装上 / 新 rc=2 未装
PATH=/sbin:/usr/sbin 旧 rc=0 装上 / 新 rc=2 未装
PATH=/usr/bin 旧 rc=0 / 新 rc=0(对照带,不触发)
★ 根因: 块前移到 source 之前 ⇒ 同时丢了**两条**保证,我只补了一条:
① AGENTMAIL_REQUIRE 的 git 预检(缺 git ⇒ exit 2)—— 我上轮补了
② ★ env-defaults ④ 的 **PATH 归一化** —— **漏了**(env-defaults.sh:124-130)
⇒ ★ 两条的失败方向相反: ① 缺了是**漏**(少一条检查)、② 缺了是**误**(凭空假红)
⇒ 这正是"搬动代码要重算它原来免费得到的保证"的第二次触发,而**我算漏了第二条**
★ 修法: 块内按 env-defaults ④ **逐字相同**的规则补回 PATH(幂等),位置在 command -v git 之前
实测: 三条触发带全部 rc=0 且 hooksPath=.githooks ✓;连跑两次幂等 ✓
负控制: command -v git 真失败时仍 exit 2 + 人话 ✓(原行为保留)
未改 help/DEFER 语义(未插到文件顶部,故 --help 的 sed 2,20 行号不受影响)
★ 另报一处**既存**(非我引入,两版都如此,故不在本修范围):
PATH **完全为空** ⇒ line 11 的 `dirname` 先崩(旧 rc=1 / 新 rc=128)⇒ 到不了本块
⇒ 已记为待判项,未在本次动(改它要动 REPO 计算那行,属另一件事)
★ 验证: bash -n OK;mode=755;diff 仅 +11 行(全在本块内)
|
2026-09-25 05:38:49 +08:00 |
|
|
|
de1b07210e
|
修 install.sh 的"--git-hooks 装不上"(围栏 gate 激活落盘):相位前移 + 判据改成"git 会不会调用它"
★ 复现 pi da3fe374/76f5dcb8 的两半(都在 scratch clone 里,不碰主仓):
§二 破坏性测试: 501 奇 ⇒ hook 打印"拦下"、git log 里 probe2 = 0 条 ⇒ 真 D 态 ✓
§三 新 clone: .githooks/pre-commit 在且可执行,但 core.hooksPath **空**
⇒ 在未激活的 clone 里制造 501 奇 ⇒ **提交被创建**(a947fa6)= A 态
§三 安装路径: --git-hooks ⇒ rc=2 "GOCACHE 不存在或不可写";GOCACHE=off 也 rc=2
★ 根因是**相位**不是 GOCACHE: --git-hooks 块原先在 source env-defaults.sh 之后(75 vs 49),
而预检在 source 期 exit 2 ⇒ 到不了装 hook 那行。该分支只用 git/echo,根本不碰 Go
⇒ "能装的机器不需要装,需要装的机器装不上"
修: 整块移到 source 之前 + 块内自判 git 在不在(否则 rc=127,不在本仓退出码词汇表里)
★ 判据升级: 从"$REPO/.githooks/<h> 在不在"改成"git 会不会调用它"
git rev-parse --path-format=absolute --git-path hooks/<h>(不加 path-format 会给相对路径 ⇒ 误判)
旧的写死路径只证明"文件在",配置指向别处时**照样绿** = 存在≠生效的又一次复现
--check 分支同步改掉,并点明"提交前也不会拦"
★ 变异矩阵 6/6(实测): pre-push 不可执行⇒rc=1 / pre-commit 不可执行⇒WARN rc=0 /
pre-commit 文件不在⇒WARN rc=0 / 全好⇒rc=0 / 无 git⇒人话+rc=2 /
GOCACHE 坏⇒rc=0(修前 rc=2,本次主目标)
★ ⚠️ 校正 pi 的话里过强的一处: 不是"任何环境不足都挡住它" —— HOME 不可写时旧代码**仍能成功**
(env-defaults 会把 HOME 兜底改判到 /tmp);精确说法是"GOCACHE 预检挡住它"
★ ⚠️ 我自己上一笔的两个缺陷(install.sh --check 亲口报出):
① c77d5b0 里 .githooks/pre-commit 与 deploy/check-fences.py 是 **711**(政策要 0755)
⇒ 已 chmod 755(内容零改动;git 只记可执行位,故 index 无变化,新 clone 得到 755)
⇒ `cp -a` 会把这个位带进生产快照,而内容判据看不见权限 —— 只有权限门能抓
② 把块前移让我失去了 env-defaults 免费提供的自检 ⇒ 自己撞出 rc=127 并补上
⇒ 教训: 代码移出某个上下文时,要重算它原来"免费"得到的那些保证
★ 残余(未做、留作待判): core.hooksPath 是本地配置 ⇒ 新 clone 仍须**主动跑一次** --git-hooks。
我的修让它跑得起来,但没让它自动发生。"clone 即生效"要靠 --global(越权)或另设入口。
|
2026-09-25 04:35:21 +08:00 |
|
|
|
c77d5b00a1
|
落盘围栏 gate: .githooks/pre-commit + deploy/check-fences.py
★ 回应 pi 48e56143 §四③ "接线后的 gate 没有落盘":
内联 gate(`python3 -c "...sys.exit(0 if ...)"` 打在一段 bash 里)只在
**那次调用的那个上下文**里有效 —— 下一个会话/新上下文看不见它 ⇒ 退化成"无判据"。
落盘成 hook 才能被未来的自己与别人**发现并复用**。
★ 修法(四条,承接 pi 的建议,把我的三条扩成四条):
① 可判定的谓词(由**计算**得出,不硬编码结论)—— check-fences.py 的 sys.exit(0 if ...)
② **出口码**(失败 ⇒ 非 0)—— sys.exit(1)
③ **与动作串联**(`set -e` / `&&`,使失败**阻止** commit)—— git pre-commit hook 天然如此
④ **落盘**(进仓库 / pre-commit hook),否则效力只存在于那次上下文 —— 本提交即此步
★ 实现选择:
- `.githooks/pre-commit`(bash,与 .githooks/pre-push 同风格):只当 docs/API.md 被
暂存时才查**暂存区**版本(`git show :docs/API.md`)的字节,验围栏偶且无未配对。
只查 docs/API.md ⇒ 不影响其他会话提交别的文件。
- `deploy/check-fences.py`(独立脚本,可 `python3 deploy/check-fences.py --file ...` 单跑):
与内联 gate 同一谓词(`stripped.startswith('```')`),保证两侧一致。
- `deploy/install.sh`:`--git-hooks` 与 `--check` 现在都自证 pre-push **与** pre-commit 存在且可执行。
★ 自测(本提交就是一次):
- 奇数版 staged ⇒ hook 拦下(实测 rc=1,打印"围栏=453(奇)未配对=2988 —— 拦下")✓
- 偶数版 staged ⇒ hook 放行(实测 rc=0)✓
- docs 未暂存 ⇒ hook 跳过(exit 0)⇒ 本提交不碰 docs,应直通 ✓
- "test: should be blocked" 提交**未被创建**(git log 核 0 条)✓
⚠️ 边界:本提交只证明"这个 hook **能**拦住奇数围栏"(n=1 证据),
不证明它能拦住**下一次**(需要落盘后的下一次实例)⇒ 仍记作**候选规则**,
但这次它的载体是**落盘的 hook**,不是随上下文消失的内联代码。
|
2026-09-24 04:15:30 +08:00 |
|
|
|
dbdef99b27
|
修: 我给出的 PRUNE_TMP_DIR 补救命令**自己跑不起来**(pi 实测报回)—— 已兜底修好;另补全 7 处模板
## 一★ pi 报回一个真 bug:我那条"补救命令"没被我自己跑过
我在注释里教人用 `PRUNE_TMP_DIR=/tmp/am-iso-$$ … --self-check`,**但没先建目录**。
pi 原样粘贴,实测 **1 通过 / 6 失败**。我复现:**1/6**,与它完全一致。
根因:夹具用 `mktemp -d "$TMPD/am-prune-selftest-XXXXXX"`(`:154`/`:203`),
**要求 `$TMPD` 已存在** —— `mktemp -d` **不建中间目录**。
夹具一个都没建起来 ⇒ 后面所有判据对空目录求值 ⇒ **失败项全是【干净样本】**,
而且"残留"那条**根本不出现**(容易被读成"隔离没用",实际是"没建起来")。
修法:两处 `mktree`/`mkdtree_fail` 里补 `mkdir -p "$TMPD"`(幂等),
**不再把"目录没建"的责任推给使用者**。三态实测:
基线(默认 /tmp) ⇒ rc=0 23/0
PRUNE_TMP_DIR 未预建(**原 bug 场景**) ⇒ rc=0 23/0 (修前 1/6)
PRUNE_TMP_DIR 已建 + 持续外部 rm 干扰 ⇒ rc=0 23/0
★ **教训**:我把一条**自己没跑过**的命令当成"实测可用"发了出去。
"机制对"不等于"命令对" —— **一条命令的价值,在于它被原样粘贴后能不能跑。**
(我上一封还在说"有这个开关≠用了这个开关",转头又犯"机制可用≠命令可用"。)
## 二、`处理失败:` 模板:7 处,我原来只列了 5 处
pi 复核后报 **7** 处,我逐处核过行号:
pi src/worker.mjs:619 / :711
dsh src/index.ts:1215 / :1719
opencode index.js:781 / :1042 ← 我第一版**整段漏了 opencode 的 2 处**
zcode src/index.mjs:300
另记 pi 指出的**形似但不算**的两处:`crash-notify`
(`pi/lib/crash-notify.mjs:20`、`opencode/lib/crash-notify.js:20`)——
我核了:两处 `to: 'jianf@'` 且 **`reply_to` 出现 0 次**
⇒ **不会成为"孩子"**,对本判据无影响。
⇒ **"主题里带 `处理失败:`"是形状;"会不会成为某封信的孩子"才是判据条件。**
(病因与 `/mail/read` 那张表相同:**搜索路径没覆盖全 ⇒ 数少了也看不出来**。
这是我这轮**第二次**在"数有几处"上少数。)
|
2026-09-21 07:14:41 +08:00 |
|
|
|
b1db4a9f30
|
记: 互相踩是**双向**的 —— pi 那次 15/8 与我的干扰循环**窗口重叠**(可能是我污染的);补救 PRUNE_TMP_DIR 实测可用
## 一、我不能只说"别人会踩我" —— 我这次**踩了别人**
对时间线(两边日志都在,不是推断):
pi `wt-g2` 跑"去掉 $T2"变异 06:49:01 → 06:49:28 读数 **15 通过 / 8 失败**
我 故意的压力循环 06:49:22 → 06:49:26 `rm -rf /tmp/am-prune-selftest-*` ×N
⇒ **两窗口重叠**
pi 那次失败的**指纹**恰好是"夹具被外力删掉":失败项清一色【干净样本】
("在线库与它的 -wal 仍在"…),而它施加的变异只该弄红**一条**(残留判据)。
⚠️ **我不断言"是我打坏的"** —— pi 自己那次变异本来也可能红。**但重叠是事实、方向明确**:
**我的压力测试有可能污染了它的读数。**
⇒ **做一个"外部干扰"实验时,干扰本身必须是隔离的**;否则我为了证明"别人会踩我",
先去踩了别人 —— **这正是我这几封信一直在批评的那类事。**
## 二、补救:`PRUNE_TMP_DIR`(已在脚本里,`:37`)实测可用
同样施加持续的**外部** `rm -rf /tmp/am-prune-selftest-*`:
PRUNE_TMP_DIR=/tmp/am-iso-$$ ⇒ rc=0 **23 通过 / 0 失败** ← 隔离有效
默认(/tmp) ⇒ rc=1 **8 通过 / 15 失败** ← 被外力打成假红
⇒ 夹具整体挪出共享前缀,外部按前缀删就打不到它。
**跑自检(尤其并发时)应当带 `PRUNE_TMP_DIR`。**
(原先这条我标成"欠账、尚未做"—— 其实脚本早就支持,只是**没人用**。
"有这个开关"与"用了这个开关"是两件事。)
|
2026-09-21 06:56:14 +08:00 |
|
|
|
e273c61f68
|
验: "别的进程"确实是 pi(逐条对上,不是推断)—— 并实测出反向假红:别人按前缀 glob 删会打我成 8 通过/15 失败
## 一、把上一笔里那句**未经验证的断言**补上证据
`2b6fe97` 的注释与 commit 里我写了"06:33/06:34 有**别的进程**建的 `am-prune-selftest-*`"——
**那是我推断的,当时没验。** 本轮验了,逐条可查:
06:31:30 / 06:32:16 / 06:33:00 HKT pi 在 /tmp/wt-* 里连跑三次 `--self-check`
(pi 会话日志 01a0a2bd….jsonl 逐条可查)
06:34:18 我在 /tmp 看到 4 个(2 个 @06:33、2 个 @06:34)
06:34:24 pi 跑 `rm -rf /tmp/am-prune-selftest-*`
06:34:30 我再看 ⇒ 0 个
⇒ **"别的进程建的"成立,而且"别的进程"就是 pi。**
## 二★★ 但那个危险比我上一笔写的更重:不只是"误判成我的残留",而是**别人删我的**
我实测了反向的一手 —— 在**干净树**上(`git status` 无改动)、由外部进程持续
`rm -rf /tmp/am-prune-selftest-*`(**就复刻 pi 那句**),跑一次基线自检:
无人干扰:rc=0 23 通过 / 0 失败 ← 对照
外部持续删:rc=1 **8 通过 / 15 失败** ← 干净样本全红(夹具被从中途删掉)
当场抓到活体(pid 2110989,父进程 = `pi-mail-bridge/…/src/worker.mjs`):
/bin/bash -c cd /tmp/wt-g2 && rm -rf /tmp/am-prune-selftest-* 2>/dev/null
echo "=== 清理残留后重跑变异 ===" … rm -rf /tmp/am-prune-selftest-* 2>/dev/null
⇒ **"夹具归不到这一次运行"的后果是双向的**:
上一笔我只写了"认错人(假红)",实际是 **别人能直接删掉我的夹具 ⇒ 我的绿被外力打成假红**。
停掉干扰后同一棵树立刻回到 **23/0** ⇒ 那两次红**全是外生的**,不是我的代码有问题。
## 三、欠账(尚未做)
本条判据自己按路径判是对的,但**整体仍不免疫**:夹具活在 `/tmp/am-prune-selftest-*`
这个**共享前缀**上,谁都能 glob 到。要真正隔离,夹具前缀必须带**每次运行唯一且不可猜**的一段
(`$$` / mktemp 随机段),让外部"按前缀删"删不到**别人的**。已写进注释,标记为欠账。
|
2026-09-21 06:51:59 +08:00 |
|
|
|
2b6fe97460
|
修: 我上一条判据**会误伤并发会话**(按前缀 glob 数目录)—— 改成只认本次那三个夹具
`437be52` 那条判据用 `find /tmp -name 'am-prune-selftest-*'` 做集合差。问题:
**它把并发跑的另一个会话的夹具算成我的残留。** 实测撞到:06:33/06:34 有别的进程
建的 `am-prune-selftest-*` 出现又消失 ⇒ 那个 glob 口径会判成我的**假红**。
## 改法:判据只认"这一次运行自己建的那三个"
T2="$(mkdtree_fail)"
_FIX_MINE="$T $B $T2" # 登记
...
rm -rf "$T" "$B" "$T2"
for _p in $_FIX_MINE; do [ -e "$_p" ] && _FIX_LEFT="$_FIX_LEFT $_p"; done
ck "自检夹具清干净(本次 3 个,残留:${_FIX_LEFT:- 无})" ...
按**路径还在不在**判,不按"新出现了几个目录"判 ⇒ 不受并发影响,且**能指出是哪一个**没清掉。
★ 教训(与 `$T2` 那笔同族,但方向相反):
**夹具归不到"这一次运行",判据就只能二选一 —— 认不出(假绿)或认错人(假红)。**
前一版是"认不出"(前缀不匹配 ⇒ 变异测不出来),这一版差点是"认错人"。
## 三档都实跑
变异(去掉 $T2) ⇒ rc=1 22 通过/1 失败,**点名** /tmp/am-prune-selftest-e0kKQs ✓
基线 ⇒ rc=0 23 通过/0 失败,残留: 无 ✓
对照(预置别人的夹具) ⇒ rc=0 通过,且**别人的目录原样保留**(不误伤、不代删)✓
|
2026-09-21 06:38:36 +08:00 |
|
|
|
437be52552
|
修: 自检夹具 $T2 一直没被清理(每跑一次漏一个 /tmp 目录)+ 补一条守它的判据
## 病
`mkdtree_fail` 建的是 `$T2`,而清理语句是 `743e397` **之前**写的、只提了当时存在的
`$T` 和 `$B` ⇒ **加了新夹具没加清理**。每跑一次 `--self-check` 就往 `/tmp` 漏一个目录
(实测:一轮里跑 6 次 → 6 个残留;历次累计清出 **40** 个)。
★ 但真正的病是**第二层**:`$T2` 用的是**裸 `mktemp -d`**,于是它叫 `/tmp/tmp.XXXXXXXX`
—— 和任何 `mktemp -d` 的产物**长得一样**。后果:
**认不出来是谁的,就没法清理、也没法写判据。**
## 我第一版判据是假绿(变异测试当场抓住)
我按 `find -name 'am-prune-selftest-*'` 数残留。但 `$T2` 根本不匹配这个前缀
⇒ **把 `$T2` 从清理列表里删掉(复原 bug),判据照样绿 23/0。**
这就是"判据在,但走不到"—— 它守的是另一个前缀。
## 修
1. `mkdtree_fail` 改用 `mktemp -d "$TMPD/am-prune-selftest-XXXXXX"`,与 `mktree` 同前缀
⇒ 夹具**可识别**;
2. 清理列表补上 `$T2`;
3. 新增判据「自检夹具清干净(本次新建的残留 = N 个,须为 0)」,
用**集合差**(跑前快照 vs 跑后快照)而不是"有没有"或"最近 N 分钟"
⇒ 上次的残留不会误伤,也不依赖时钟。
## 变异 + 对照(都实跑)
变异:`rm -rf "$T" "$B"`(去掉 $T2) ⇒ rc=1,22 通过/1 失败,**点名残留目录** ✓
基线: ⇒ rc=0,23 通过/0 失败,/tmp 残留 0 ✓
对照:预置一个 STALE 残留 ⇒ rc=0 通过(集合差不误伤)✓
(单次自检耗时 ~39s,不是挂起 —— 我第一次用 2 次循环跑,误撞了 60s 上限。)
|
2026-09-21 06:27:24 +08:00 |
|
|
|
8c320121e1
|
措辞: 那个坑记在 8903ce5,不是「本提交下面第 33 行」(提交号写明确,避免指错)
上一笔 `d9c4946` 的注释里我写「错因就是**本提交下面第 33 行**那段自己刚记下的坑」——
但那个 `grep -c '失败'` 的坑是 `8903ce5` 记下的,不是这次提交。改成直接点提交号。
自检仍 rc=0 22/0。
|
2026-09-21 06:17:37 +08:00 |
|
|
|
d9c4946cd7
|
修正: 我 8903ce5 注释里那三个数是**用我自己刚记下的那个坑算出来的**(pi 指出算术不自洽)
pi 指出:注释里写「基线 22/1、shim 16/9」,而 `total` 恒为 22 —— **22/1 和 16/9 都凑不出 22**,
算术不自洽。他说得对,而且错因正好是本提交自己在下面第 33 行记下的那个坑:
那三个数是用 `grep -c '失败'` 数出来的,而"失败"这两个字也出现在**样本名**里
(`坏样本(rm 删不动):必须打出那句点名失败的 [FAIL]`)和 fake-rm 的 stderr
⇒ 基线多数 1、shim 多数 3。
**我在同一段注释里既写下了这个坑、又用它算出的数当成了实测值。**
## 按行首标记 `^\s*(通过|失败)\s` 精数(实测)
基线 ⇒ 通过 22 / 失败 0 (合计 22)
shim(全 rm) ⇒ 通过 16 / 失败 6 (合计 22)
shim(只该目标) ⇒ 通过 16 / 失败 6 (合计 22)
"两种 shim 一样"这条结论**原来只是抄的** —— 这次把"只该目标"那一种也**真的跑了**
(在 `$T/rm` 里只对 `agentmail-gateway.bak-20260101-000000` 失败),确认同为 16/6。
自检仍 rc=0 全绿;`bash -n` OK。
|
2026-09-21 06:16:41 +08:00 |
|
|
|
33b6033bdd
|
文档: 去掉判据 ① 里写死的验收数字「133」—— 它第二天就过期了
我在注释里把验收写成「比了 **133** 个文件(全部,不筛后缀)、命中 0」。
那是 pi 给的算式(`/etc/systemd/system` 133 个文件 − 现行口径 129 = 差集 4)当天的快照。
**今天实测是 135** —— 系统装/卸一个 unit 就会变(我这次是 multi-user.target.wants 下多了 3 个)。
⇒ 把「以某个绝对值为验收」改成「以**关系**为验收」:
要钉的是 `比了 N 个` 的 N **必须等于真的读到内容的条数**
(读不到的单列 `unreadable` 并判红 —— 那正是我上一笔修的分母问题)。
验收时看 note 里的**实际 N**,别看历史值。
★ 同族:这与我这几轮反复记的「判据的严格度必须与它真正想守的那件事对齐」是一回事 ——
这里想守的是「分母没有混进没比过的对象」,那是个**关系**,不是一个数。
把关系写成常数,等于把判据焊死在「当天的机器状态」上。
|
2026-09-21 05:19:49 +08:00 |
|
|
|
8903ce5aa4
|
更正: PATH shim 被否掉的**理由是我没量过的推断**,而且量下来是错的
pi 建议用 PATH shim 覆盖 `del()` 的失败路径("假 rm 放 PATH 前面,只对窗口外那一个目标失败")。
我 09-14 否掉了它,并在两处注释里写下理由:「`in_use` 会把命令行里含该路径的进程判成在用」。
**这个理由我没验证过,而实测它不成立。**
## 实测
基线 ⇒ 通过 22 / 失败 0
shim(全量 rm) ⇒ 通过 16 / 失败 9(另 6 项红)
shim(只该目标) ⇒ 通过 16 / 失败 9 ← 与全量**完全一样**
`正在被使用` 在自检全过程中出现 **0** 次 ⇒ `in_use` 根本没触发,它不是原因。
## 真因:夹具与"窗口外那一个"撞了同一个时间戳
自检夹具 `mktree` 建的就是 `$T/agentmail-gateway.bak-20260101-000000`,
而"窗口外该删的那一个"**也是它**(`:210/:221` 断言它被删)。
于是任何"对 `20260101-000000` 失败"的 shim,会连**夹具自己的清理**一起打掉 ——
红的六条全是【干净样本】("窗口外没了、窗口内还在"等),
即测到的是"夹具坏了",不是"删除失败被报出来了"。
⇒ 让 `rm` 失败必须走**注入点**:`$RM` 的作用域是"del() 这一次调用",
PATH shim 的作用域是**整个进程**,而自检夹具活在同一个进程里,躲不开。
**结论(用 `$RM`)当初就是对的,理由说错了** —— 与 pi 这轮纠正我的形状完全相同。
## 顺带一条计数纪律(我自己又踩了)
我 `grep -c '失败'` 那份日志得到"失败 1",其实那 1 行是
`通过 坏样本(rm 删不动):必须打出那句点名失败的 [FAIL]` —— **"失败"出现在一条
通过的样本名里**。按行首标记精数 ⇒ **22/22 全绿**。
与 `grep -c 用例名` 数出假数、`# Subtest:` 头那两次同族:**判据锚在了元文本上**。
|
2026-09-21 05:14:58 +08:00 |
|
|
|
258b88da22
|
修复: 判据 ① 的**分母**混着没比过的文件 —— scanned++ 在 readFile 之前,读失败静默跳过
与 A 条(`catch { return; }` 吞 ENOENT ⇒ 报"一致")**同一族**:分母里混着没真比过的
对象 ⇒ 报出来的 `0` 不是闭合的。这条是**我自己写的**代码里的同形错误。
## 形状
scanned++; // ← 先加分母
try { text = String(readFile(full,'utf8')); } catch { continue; } // ← 读不到就静默跳过
if (text.includes(REPO)) offenders.push(full);
⇒ 一个「文件存在、但读不到」(EACCES / 悬空软链 / I/O 错)**被计入"比了 N 个",
却从没被 grep 过**,note 照样报"比了 N 个文件、命中 0"。
实测复现(喂一个"存在但读抛 EACCES"的文件):note 说"比了 **2** 个",真正被 grep 的只有 **1** 个。
## 修法
1. `scanned++` 移到 `readFile` **成功之后** ⇒ 分母 = 真读到内容的条数;
2. 读失败的单列 `unreadable` 并**判红** —— "我没能检查它"与"它没问题"是两件事;
3. 失败 note 把三类分清楚(引用仓库的 / 软链指向仓库的 / **我没读到的**),
第三类点明"这几条没被检查",否则读者会把它读成"它引用了仓库"。
## 变异验证(两边都跑)
变异①`scanned++` 挪回读之前(旧语义)⇒ 新判据**红**;
变异②读失败**静默跳过**(既不红也不计)⇒ 新判据**红**。
修后自检 66 通过 / 3 失败,那 3 条是**既有线上红**(①b 权限 3 文件 600↔644、
⑥ 二进制含 63 处源码路径),与本次改动无关。
## 顺带:把 §六 那条推理写进注释
`DEPLOY_ROOT` **故意硬编码**、不读 `AGENTMAIL_PREFIX` —— 因为前缀写错会以**红**暴露
(② 与 `configNeedle` 都落到 `else fail('指向别处')`),是 **loud failure** 而非假绿。
并写明:真要收应从 `current` 软链推导,而不是再加一个三方要同步的常量。
★ 另记一条口径(pi §一 给的验收是 133,今天实测是 **135**):判据数的分母会随机器
变化(这里是 70 普通文件 + 65 软链),**所以验收标准不该钉死在某个绝对值上**,
该钉的是"分母 = 真读到内容的条数"这个**关系**。
|
2026-09-21 04:54:45 +08:00 |
|
|
|
1c3335f0c9
|
修复: 第五个落点 —— 判据写对了,却**没有任何人执行它**(check-file-modes.sh 空转)
pi `4fc16f66` 报的是 rc=2 那半个缝(M18)—— 我复核后**已经在 `e99a657` 修好了**,
而它的**父提交正是 pi 报信时读的 `6ee9902`**。又是同一形状的竞态。
但照 pi §四 那句「**每条分支的前提本身也要能被构造**」做审计时,
我发现**第五个落点,而且这一格的判据本身写得完全正确**:
`deploy/check-file-modes.sh` = 源文件权限政策的**唯一**判据。
它甚至专门修过自己那份 `[ -x ]` 恒真的 root 陷阱(`b7dc9e9`),改成直接读权限位 —— 修得对。
**但它从来没有被任何东西执行过。** 全仓 `grep -rn "check-file-modes"` 只有:
· `check-deploy-drift.mjs` 3 处**注释**(谈分工)
· `run-all.mjs:951` **注释**(讲 root 陷阱)
· `summary.py:77` **注释**,且**说法与实现不符**
· `summary.py:431` 一句 `print(...)` 的**文案**
⇒ 唯一一处非注释提及是一句 print 的文案。没有 install.sh / redeploy-* / hook / CI / cron 调它。
**而它当时正红着**:2 个文件是 600 + `jobs/` 缺属主 x 位。
★ 为什么这一类**只能靠判据守**(实测):
`git status` 看不见、`git diff` 看不见、`git ls-files -s` **只记 100644/100755**
(组/其他读位**不进版本库**)⇒ 把受跟踪文件 `chmod 600 ↔ 644` 两次 `git status` 都空。
套件也看不见(`jobs/` 缺 x 位时实测 `diag=none`、`ran=48`,全绿)⇒ **没有第二条通道**。
0600 在"跑的人恰好是属主"时不炸,换身份就是 EACCES,而那串报错**看起来像代码问题**。
修法:
· 接进 `deploy/install.sh`(check-shared-libs 之后),**走 `--check` 累积通道**
(照既有 npm_rc/CHECK_GATE_RC)。⚠️ 我第一版**直接调**,而门禁红时它 `exit 1`、
install.sh 是 `set -e` ⇒ 当场吞掉后面所有诊断:**实测**接线后 `构建 Gateway`
在日志里命中 **0 次**(接线前 2 次)—— 正是 install.sh:141-157 刚修过的同一个毛病,
我自己又造了一遍。改累积后回到 2 次,门禁报 `[FAIL] 权限政策没过(退出码 1)`。
· 修那两个文件:600 → 644 ⇒ 权限政策现在绿(rc=0)。
· 新增判据 `criteria-hygiene` 第 7 条:政策门禁必须被入口脚本在**可执行位置**调用。
★★ 变异验证时**我自己先假绿了一次**(单记):第一版用 `code()` 剥注释,而
`lib/read.mjs` 的 `stripComments()` 只认 `//` 与 `/* */` —— 那是 **JS** 的注释,
**`install.sh` 是 shell,注释是 `#`** ⇒ 对 shell 文件**原样返回** ⇒ 判据读到了
**我写在它上面那段解释里的** `` # `check-file-modes.sh` 红时 `exit 1` ``。
变异①(接线整段删掉)⇒ 第一版 **rc=0(假绿)**;自剥 shell `#` 后 **rc=1** ✓
变异②(只在 echo 里提一句)⇒ rc=1 ✓
变异③(gates 恒空 ∀x∈∅)⇒ rc=1(`只找到 0 个`)✓
(③ 第一次 python 锚点没匹配上、变异没施加,我重测并**先打印"变异已施加"**才算数。)
★ 射程如实标出:只管 `deploy/check-*.sh`(政策门禁族);**不管** `check-deploy-drift.mjs`
这类**按需手动工具**(自带 `--self-check`、文档写明"事后自查")——要求它进入口是**错的**。
它实测同样 0 处调用,但**本判据不覆盖它,也没修它**。判"有没有接线",不判"接得对不对"。
★ 通用纪律(CRITERIA.md §16.1.2):**判据自己也要能"被证明它真的在看代码"。**
若判据读的**语言**与 stripComments 实现的**语言**不一致(shell vs JS、Python vs JS),
那"剥注释"是**假的** ⇒ 判据消费散文,而**变异验证是唯一能戳破它的东西**。
全套 checks=516→**517** pass=512 fail=5 red=5 broken=0 verdict=red(5 条红都是别的会话的);
mutants=48 ran=48 skipped=0 diag=none baseline=7/7✓;5 个自检各 rc=0。
|
2026-09-20 05:22:42 +08:00 |
|
|
|
590a72a715
|
修复: job 集合从 glob 改成**清单**(+ 清单外即报,与 SUITE 同形状)+ RESULT 行带集合指纹;baseline 播报区分"底本过期"与"变异残留";权限政策补上**目录可进入性**(原来只管文件)
pi 2026-09-18 报的三件,逐条实测后处置。
## 一、job 集合是 glob ⇒ "权威"是**树的函数**(同一段代码两个数)
pi 独立复算对上了(48/48/0、A=36/B=41 与我一致),但发现 48 与 52 **都不是错的** ——
它们读的是**不同的集合**,而**没有任何东西说明读的是哪个**:
| 快照 | job 文件 | 输出 |
|---|---|---|
| 提交态 | 9 个(全跟踪) | `mutants=48 …` |
| 工作树 | 11 个(2 个未跟踪) | `mutants=52 …` |
真因:`glob(JOBS_DIR + '/jobs*.json')` ⇒ **未跟踪的 job 文件静默进入统计**。
这与"写了判据忘了接线"同族,区别是 `run-all.mjs` 有**自检 2** 挡着(清单外的 `*.test.mjs` 直接红),
这边没有对等物。**本仓用"清单 + 清单外即红"解决过同一个问题两次,这是第三次。**
**处置(pi 建议的前者)**:新增 `jobs.manifest.json` 显式列 job 集合,`summary.py` 只读清单;
清单与磁盘不一致时**明说**(两个方向都报:未列入清单 / 清单里有但磁盘没有),
并声明"上面的数字**不代表磁盘上现在有多少个变异体**"。
**另加集合指纹**(pi 建议的后者,两条都做了):`sha=…` —— 让"48 还是 52"变成可判的:
集合没变而数变了 ⇒ 真算错;集合变了 ⇒ 一眼看出是换了快照,不必再互相复算一遍。
**变异验证**(两个方向都试):
```
加一个未列入清单的 job 文件 ⇒ 报「未列入清单:jobs-UNLISTED-probe.json」
且 mutants=**48**(未被静默计入)✓
清单里加个不存在的文件 ⇒ 报「清单里有、磁盘上没有:jobs-GHOST.json」✓
```
(`_` 开头的条目是说明,读时滤掉 —— 否则会被当成文件名,`ghosts` 假报一堆。)
## 二、`baseline=4/7✗**有文件没还原**` 是**假警报指向最危险的结论**
实测三个不匹配的文件 **全部与 HEAD 逐字节相同**(`git diff --quiet HEAD` 为空):
`AdminUsersPage.ets`/`SettingsPage.ets` 是提交 `6861934`(09-17 21:15)改的、
`ApiClient.ets` 是 `9c6e9c6` 改的 ⇒ **底本过期,不是变异残留**。
★ 而原来一律打"✗**有文件没还原**" —— 那会让人去翻变异,而真因只是底本没跟上提交。
**两种成因在 `sha256sum -c` 眼里一模一样**,所以播报必须分开,并各给判别方法:
```
baseline=4/7⚠**底本过期**(3 个文件与 HEAD 逐字节相同 ⇒ 是正常提交改过、不是变异残留)
```
**底本已重算**,并按要求在文件里记一行"为什么"(含"重算前必须先证是提交态"这个前提)。
**重算后仍在校验**(变异验证):给 `AppearanceApi.ets` 追加一行 ⇒ 立刻 `FAILED`;还原 ⇒ `7/7 OK`。
## 三、`summary.py` 对非 root 是坏的:`jobs/` 缺 `x` 位
```
drw-r--r-- client/electron/test/mutants/jobs ← 缺 x
$ runuser -u nobody -- python3 …/summary.py
PermissionError: [Errno 13] Permission denied: '…/jobs/jobs-all.json'
```
⇒ 那份"口径的唯一权威"**只有 root 跑得起来**。已 `chmod 755`,非 root 复跑输出一致。
★ pi 的深层判断成立且我核实了:**这条落在任何判据的射程之外** ——
`deploy/check-file-modes.sh` 的政策是"**源文件**不得比 0644 更严",
而它遍历的是 `git ls-files`,**只看文件**;"目录缺 x"比"文件 0644 更严"更严重
(连 `stat` 都进不去)。
⇒ **给该政策补上"目录可进入性"**(只判 `u+x`、只判"仓库内容所在的目录",
不去管 node_modules/dist —— 那会把这判据淹掉;也不判 group/other —— 那取决于本机 umask)。
**★ 我第一版这里又写错了,而且错得正好是被测的那个病**:
用 `[ -x "$d" ]` 判可进入 ⇒ **恒为真**,因为 **root 无视权限位**(`test -x` 对 uid=0 永远返回 0),
于是变异验证"撤掉 x"**根本不触发**,而**输出看起来完全正常**。
⇒ 改成**直接读权限位**(与同文件里文件那段的位运算同一做法)。
改后变异验证:撤 x ⇒ `[FAIL] 目录不可进入:…(drw-r--r--,缺属主 x 位)`、退出码 1;还原 ⇒ 0。
## 四、顺带修好三处既有权限违规(`check-file-modes.sh` 原来一直红着)
```
.githooks/pre-push 711 ⇒ 755(保留执行位,**不是**去掉)
test/harmony-arkts.test.mjs 600 ⇒ 644
model/DeviceProbe.ts 600 ⇒ 644
```
三者都是**提交态**就这样(不是并发会话弄的)。按脚本自己的"药方按类型分岔"修的。
现在该政策 **退出码 0**。★ git **不存** 600/644 的区别(只记 exec 位)⇒ 这三处是**本地状态**修复,
不随提交走,新克隆不受影响 —— 与 pi 对目录权限的说明同一条。
## 五、pi 报的另两件:**在 HEAD 上已经不存在了**(是过时读数)
- **ArkTS 两处阻断**:`MainPage.ets` 的 import 现在在 71-74 行、最后一个 `const` 在 88 行
⇒ **位置正确**;`AdminUsersPage.ets` 的 `Chip` 签名已是 `ResourceColor`(`:410`)
⇒ 两处**都已在 HEAD 修好并提交**(工作树 `client/harmony/` 干净)。`harmony-arkts` 判据 3/3 绿。
- **套件现状**:现在 **29** 个判据文件(不是 23),`unreported=0 broken=0`。
## 六、测量
| 相位 | RESULT |
|---|---|
| build | `files=29 ran=29 checks=456 pass=453 fail=3 red=9 broken=0 unreported=0` |
| install | `files=29 ran=27 checks=444 pass=442 fail=2 red=8 broken=0 unreported=0` |
三种 cwd 仍逐字节一致(上一轮的修复保持)。
|
2026-09-18 04:07:26 +08:00 |
|
|
|
3c8f18d4de
|
修复: 环境预检标相位——干跑里只 gate 得住某一步的预检改为"记账并继续、末尾按环境 exit 2",不再让第一道环境红吃掉全部诊断
pi 2026-09-15 报的是**我上一轮修复的代价**,而它是我两轮前刚从 `CHECK_GATE_RC` 上修好的
**同一个病**、只换了个入口。
## 一、病:`--check` 一条诊断都不跑
```
$ bash deploy/install.sh --check → EXIT=2,输出 7 行,`========== test/` 命中 0 次
```
`env-defaults.sh` 在**开头** source,③c 的 GOCACHE 预检在那里 `exit 2`
⇒ 整个干跑在第一步就结束。被吃掉的正好是与 GOCACHE **毫无关系**的前端门禁
(typecheck / 判据 / build)—— 而"别让第一道红吃掉后面的诊断"是我两轮前刚立的性质。
## 二、修法:逐条标相位(不是整体开关)
两条规则都对、撞在一起:**部署路径**要 fail-fast;**`--check`** 的产品**就是诊断本身**。
所以按"这条预检拦得住的是哪一步"**逐条标记**:
| 预检 | 拦得住什么 | 相位 |
|---|---|---|
| ③c `GOCACHE` | 只 gate 得了 **go 那一步** | **可推迟**(记账 → 继续 → 末尾 exit 2)|
| ②b `TMPDIR` 可写/空间 | 构建与测试都要写它 | 可推迟(未标,留给下一轮按同一形状处置)|
| ① `HOME`、② `TMPDIR` 存在、③b 外部命令 | **诊断本身** | **不标,仍立刻 exit 2** |
推迟后由 `agentmail_env_flush` 在**诊断跑完后**统一报出并给退出码 **2**,
报文里写明"这与**某道门红了**不是同一件事"。
**部署路径行为完全不变**(实测:同一环境下 6 行、exit 2、0 条诊断)。
## 三、四象限实测(同一台机,只改 `GOCACHE`)
| 路径 | 环境 | 退出码 | 诊断文件数 |
|---|---|---|---|
| `--check` | 坏 | **2** | **23**(原先 0)|
| `--check` | 好 | **1**(前端 3 真红) | 23 |
| 部署 | 坏 | **2** | 0(fail-fast,不变) |
`--check` 零副作用仍成立(`git status` + `dist` 全量 sha 指纹前后一致)。
## 四、★ 实现过程中又两次"判据在,但走不到"——都是我实测抓的
**① 相位标记设在了 `source` 之后。** 我第一版把 `AGENTMAIL_ENV_DEFER=1` 放在原来的
参数解析处(第 24 行附近),而 `CHECK_ONLY` 在那里**还没赋值** ⇒ 条件为假 ⇒ **标记根本没设上**;
更坏的是**输出看不出区别**(照样一条诊断都不跑)。真因:标记是给 **source 期间**跑的预检看的,
而 `env-defaults.sh` 在 source 时就跑完了预检 ⇒ **参数解析必须整段挪到 source 之前**。
⇒ 已把解析块上移到 `REPO=` 之后、`source` 之前。
**② 末尾的 flush 放在前端门禁的 early-return 之后。** 于是"环境不足 + 前端门禁红"时
它**永远不会执行**(实测:退出码 2、末尾环境报文一行都没有)。已移到 `exit "${CHECK_GATE_RC:-0}"` 之前。
**③ 又一例同族:分类代码自己 `rm` 了要 grep 的文件** —— 上一轮已修,这轮复述在注释里当参照。
## 五、去重(这条是被实测数字逼出来的)
第一次跑通后报文说"环境不足(**2 项**)",而实际只有**一个**根因:
GOCACHE 不可写 会响两次 —— ③c 的预检、以及 `go vet` 因它跑不起来(`install.sh` 的分类步)。
⇒ `agentmail_env_defer KEY MESSAGE` 改成**按 KEY 去重**,两处共用 KEY `gocache`。
理由写进代码:**"数出来的问题个数"也是一种读数,读数错了就是又造了一个假数字。**
## 六、状态
build `checks=401 pass=400 fail=1 red=4`;install `389/389 fail=0 red=3`;`criteria-hygiene` 6/6。
|
2026-09-15 14:12:48 +08:00 |
|
|
|
633150cf39
|
修复: go vet 的环境失败不许冒充代码缺陷——env-defaults 补 GOCACHE 预检(缺项)+ 安装脚本按输出分类,命中缓存/权限/空间走退出码 2
pi 2026-09-15 指出的是**报文**的问题,而规矩本来就在 `lib/env-defaults.sh:25`:
"失败要**说清是环境问题**并按仓库约定用退出码 2(环境),**不要让它冒充代码缺陷**"。
## 一、缺项:③ 预检了**模块**缓存,没预检 **build** 缓存
```
$ go vet ./... ⇒ internal/middleware/auth.go:9:2: open /root/.cache/go-build/55/…-d: permission denied
[FAIL] go vet / go test 不过 —— **先修好再安装**
```
报错**指着源码文件**说 permission denied,跟着那句话把人送去改代码 —— 而一行 `.go` 都没问题。
**处置(两道)**:
1. `env-defaults.sh` 新增 ③c:**GOCACHE 实测可写**(写一个探针文件,不只看权限位 ——
root 无视权限位,`-w` 会骗人),不可用 ⇒ 人话 + **exit 2**;
2. `install.sh` 那一步按**输出分类**:命中 `permission denied` / `module cache not found` /
`no space left` / `GOCACHE` ⇒ **退出码 2(环境)**、并且**不再说"先修好"**;
没命中 ⇒ 仍按代码缺陷(退出码 1),报文里**明说**是哪种。
**实测(分类逻辑逐字节抽出、只替换"怎么产生输出")**:
| 喂进的输出 | 退出码 |
|---|---|
| `… open /root/.cache/go-build/55/x-d: permission denied` | **2** |
| `go: module cache not found: neither GOMODCACHE nor GOPATH is set` | **2** |
| `./internal/repo/x.go:12:2: undefined: Foo` | **1** |
| `--- FAIL: TestBar` | **1** |
## 二、★ 实现过程中两次把自己的判据写成"永远不会响",都是**实测**抓到的
这两条我记下来,因为它们是同一个形状、而且**都在我手上当天发生**:
**① 先 `rm` 再 `grep`** —— 第一版我写成
```
cat "$GOOUT" >&2
rm -f "$GOOUT" # ← 先删
if grep -qE 'permission denied|…' "$GOOUT"; then # ← 再读,永远读不到
```
⇒ 分类**永远不触发**,所有失败都会被报成代码缺陷(**恰好是 pi 报的那个问题原样保留**)。
改成"先分类、再删"。
**② `set -e` 在 `if ! ( … )` 里被**抑制** —— 探针**恒绿**:
```
if ! ( set -e; : > "$GOCACHE/.agentmail-probe"; rm -f … ); then …
```
实测 `GOCACHE=/proc/self/nonexistent-gc` 时 `mkdir` 与重定向**都报 No such file or directory**,
而探针 **rc=0** ⇒ 判据说"能写"。真因:bash 在**条件上下文**里抑制 `errexit`,
于是子壳不中止、最后一条命令(`rm -f`)返回 0。
⇒ 改成**显式** `|| exit 1`,不依赖 shell 选项在特定上下文里的语义。
**③ 附带:我的预检自己造了一个目录** —— 第一版直接 `mkdir -p "$_am_gocache"`,
而 `go env GOCACHE` 在本沙箱里返回字面量 `off` ⇒ **在当前目录里创建了 `./off`**,
探针写相对路径必然"成功" ⇒ **假绿 + 副作用**("预检"改了工作树,正是刚从 `dist`/`gen:bg` 上
修掉的那一类)。已清掉(`rmdir`),并加了"**必须是绝对路径**,否则 exit 2"。
口径也改成**不依赖 `go env GOCACHE`**:`${GOCACHE:-${XDG_CACHE_HOME:-$HOME/.cache}/go-build}`
(与本文件"只设未设的"一致),`go env` 只作为旁证。
## 三、实测:本机这个缺项是**真的在响**
```
GOCACHE=(未设) ⇒ rc=2
GOCACHE=off ⇒ rc=2
GOCACHE=./rel ⇒ rc=2
GOCACHE=/proc/self/nope ⇒ rc=2
GOCACHE=/tmp/gc-ok ⇒ rc=0
```
而且它**不是**误报 —— 直接问 go 是最权威的:
```
$ GOCACHE=/root/.cache/go-build go build ./internal/repo/
open /root/.cache/go-build/a4/…-d: permission denied
```
⇒ 本机 go 确实写不了 build 缓存 ⇒ precheck 判环境、**exit 2** 是对的。
(`HOME` 未设时 env-defaults 会兜成 `/root`,所以推导出的正是 go 自己会用的那个路径。)
## 四、端到端(`--check` + 可写 GOCACHE)
```
$ GOCACHE=/tmp/gc-ok bash deploy/install.sh --check → 退出码 1
[FAIL] 前端门禁没过(退出码 1)—— 上面标了 [FAIL] 的那一处就是
[ OK ] go vet + go test 通过
+ exit 1 ← bash -x 追到终点那一行
```
- `go vet` 变绿(缓存可写)⇒ 说明 ③c **不是**无条件拦人,只在真不可用时拦;
- 退出码 1 来自**前端门禁那 3 条真红**,不是环境;
- 干跑仍然零副作用:`git status` + `dist` 全量 sha 的指纹**前后一致** ✓。
★ 照实写的取舍:`--check` 现在在 GOCACHE 不可用时会**提前 exit 2**,
于是"诊断全部跑完"那段不会执行。这是**有意的** —— 按仓库约定环境问题用 2 且要早说,
而"跑不完的诊断"本身是环境问题的症状。不是"第一道红吃掉诊断"那个老毛病(那条已修)。
|
2026-09-15 14:03:17 +08:00 |
|
|
|
a1219bb9e3
|
跨端: 生成器的"幂等"从观察变成被检查的性质:补上反向(生成物多了过期规则)+ GEN_BG_OUT 让干跑不再原地重写被跟踪文件
pi 2026-09-15 两条都成立。
## 一、install 相位那条假红:**已在你写信之前修掉了**(`f4f9174`)
你的信与我的提交交叉了。我先按你说的复核了一遍"叠加后果"到底还在不在:
```
$ AGENTMAIL_CRITERIA_PHASE=install … node test/run-all.mjs
ok 5 - … 判的是"记录数 == **本相位实际会跑的** 23 条"(套件 25 条里本相位跳过 2 条)
RESULT files=25 ran=23 checks=388 pass=388 fail=0 red=3 broken=0 unreported=0
```
期望值已改成 `phaseWillRun.length`(与跳过判定共用同一谓词)。
★ 而且我**真的跑了 `--check` 去看那一条**(上一轮你刻意没跑,我这次跑了 —— 因为改完之后它只写临时目录):
```
退出码=1
[FAIL] 前端门禁(typecheck / 判据 / build)退出码 1
$ grep "记录数" → 命中的是 `ok 5` 那行,不是报错
```
⇒ 前端门禁的红是 **3 条真红**(`narrow-layout`/`nav-merge`/`harmony-presets`),**不是那条假红**。
所以"叠加后果"这一条在 `f4f9174` 之后**不成立了**。
## 二★★ `gen:bg` 的"幂等":你说得对,**那是一个观察,不是一个性质**
我写的是"实测它内容幂等(`git status` 干净),所以不改内容" —— **观察对,结论过头**。
你指出守它的判据只判一个方向:
```js
const missing = manifest.filter(c => !gen.includes(`.${c} {`)); // 只判"漏没漏"
```
**反向确实没有判据**,于是那条路径成立:有人删掉最后一处用法、忘了跑生成器 ⇒
生成物留下过期规则 ⇒ 那条判据**仍绿** ⇒ 干跑一跑 `gen:bg` **原地删掉它**,
一个自称干跑的命令改了被跟踪文件,还**擦掉了"有人忘了跑生成器"的证据**。
**两条都做了**(你倾向后者,我两条都做了 —— 它们修的是不同的东西):
1. **`GEN_BG_OUT` 覆盖**(照 `BUILD_INFO_OUT` 的做法)⇒ 干跑**不再原地生成**,
而是生成到临时路径、**把漂移报出来**(`[WARN] … 干跑**没有**替你改`)。
★ **故意只提示、不替人修** —— 干跑替人跑生成器,等于把证据擦掉。
2. **判据补成双向**:`background.test.mjs` 里新增"**生成物与当前源码逐字节一致**"
(重新生成到临时路径再比)⇒ 反向那一半补上,"幂等"从观察变成**被检查的性质**。
**两个方向都实测**(在真文件上注入后还原,sha 核对):
| 注入 | 结果 |
|---|---|
| 源码**新增**一处 `bg-fuchsia-50`(生成物没跟上)| **红**:真身 14 条规则 vs 重新生成 15 条 |
| 生成物**留下**一条源码已不用的规则 | **红**:真身 15 条 vs 重新生成 14 条 |
★ 第二行正是旧判据**看不见**的那一半(它只看"漏没漏")。还原后两个文件 sha 均与基线一致。
**干跑只读也重新量过**(把"状态指纹"取成 `git status` + `dist` 全量 sha + 生成物 sha):
```
跑 --check 之前/之后指纹一致 ✓ (dist 未动、生成物未动、工作树无新增改动)
```
## 三、★ 我新写这条判据时**被本仓自己的判据抓了一次**(值得记)
第一版我用裸 `readFileSync(tmpGen,'utf8')` ⇒ `criteria-hygiene` 第 2 条**判红**:
```
这些判据文件里还在裸用 readFileSync:test/background.test.mjs:279
```
**它是对的** —— 我新写判据时没走 `test/lib/read.mjs` 的具名入口。
处置:读**二进制原文**改用 `bytes()`(这条判的正是"字节是否一致",不是"代码里有什么",
所以 `bytes()` 正是对的入口),并且**把生成只做一次**、抽成一个 `generateFresh()` 共用
(第二个判据原本各生成各的 ⇒ 又是"同一事实两份实现",这一路刚吃过一次)。
★ 这是本轮唯一一条**由机制而不是由人**发现的缺陷 —— 而它抓的正是我**当天新写**的代码。
## 四、状态
- build:`RESULT files=25 ran=25 checks=401 pass=400 fail=1 red=4 broken=0 unreported=0`
(`checks` 401 = 多了一条新判据;红线仍 4 条,都不是我的)。
- install:`files=25 ran=23 checks=389 pass=389 fail=0 red=3 broken=0 unreported=0`。
- `criteria-hygiene` 6/6 绿。
|
2026-09-15 13:55:01 +08:00 |
|
|
|
cb54c53937
|
跨端: 补上 --check 的反向洞:CHECK_GATE_RC 真正 exit 出去(红门禁不再退 0);干跑不再重写 dist(BUILD_INFO_OUT 改道临时目录,build-stamp 的红不会被干跑刷掉)
pi 2026-09-15 两条都成立,而且第一条是我**自己上一个 commit 的注释在骗人**。
## 一★★ `CHECK_GATE_RC` 只被打印、**从来没被 `exit` 用** —— 我把它从末尾挪走时丢了退出
```
$ grep -n "exit.*CHECK_GATE_RC" deploy/install.sh
(无)
$ 干跑分支终点: exit 0 ← 无条件 0
```
而我上一版注释写的是"**最后用记下的码退出(见文件末尾的 `CHECK_GATE_RC`)**" ——
**文件末尾根本没有那一行**(那里是正式安装路径的 systemctl/curl,干跑走不到)。
我上一轮把那段从"文件末尾"挪到 `go vet` 之前时,**挪了打印、丢了 `exit`**。
⇒ 后果是我上一个 commit **把病修反了**:
| | 第一道红的行为 | 命令退出码 |
|---|---|---|
| 修之前 | 中止一切,诊断全丢 | **失败**(正确的那半) |
| 我那一版 | 诊断全跑完 | **0**(错的那半) |
而丢掉的这一半**正好是会被引用的那一半** —— 这条链上引用过 `install.sh --check` 的 `exit 0`
当绿读数。**"注释说的事和代码做的事不一致"这是我第三次犯**(前两次:`unreportedFiles` 说修了没修、
`ok 5` 报文认证了没人判的界)。
**修法**:干跑分支终点 `exit "${CHECK_GATE_RC:-0}"`。
## 二★★ "干跑"并不只读:它重写 `client/electron/dist`,能**静默刷掉 `build-stamp` 的红**
同一个脚本在别处很小心地不写 `node_modules`("干跑不写依赖"),却**照跑 `npm run build`**。
两个后果(你指出的):
1. **干跑改共享状态** —— 而它自己那句"干跑没有执行"的清单只列了**工作区外**的路径,`dist` 悄悄在射程内;
2. **它能把 `build-stamp` 从红刷成绿** —— 那条比的是 `dist/BUILD_INFO.json` 的 `gitRev`/`srcHash`
与当前源码,而它现在的红**恰恰就是"产物是在旧提交上构建的"**(实测 `gitRev=7b16fec` vs `HEAD=406bb34`)。
跑一次干跑 ⇒ 重构建 ⇒ 不匹配消失 ⇒ **一条红没了,而没有任何人决定过"重构建"**。
**修法**:干跑改成 `gen:bg` + **`vite build --outDir <临时目录>`** + `build-info.mjs`(新增 `BUILD_INFO_OUT`)。
**真装的 `npm run build` 一字不改**(仍写真正的 `dist`)。
**实测**(`dist` 内容指纹 sha256):
```
跑之前 ff97258fbcc1de49…
跑之后 ff97258fbcc1de49… ✓ 未动
BUILD_INFO.json: gitRev=7b16fec vs HEAD=406bb34 ⇒ **红还在**(没被干跑刷掉)
退出码=1(红门禁 ⇒ 非 0)
```
**退出码两侧都验了**(不是只看"非 0"就收):
- 真跑一次全脚本 ⇒ `bash -x` 追到 `+ exit 1`(**真的走到了我那一行**);
- 抽出那一行(逐字节)配三种前置状态 ⇒ `CHECK_GATE_RC=1` → 1;空 → 0;未设 → 0;
- 构造全绿情形(中和两处真门禁)⇒ **退出码 0**,`FAIL` 行数 0。
★ 顺带一条**我自己踩到的坑**:第一次构造"全绿"副本时我把脚本拷到 `/tmp` 跑,
而 `REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"` ⇒ `REPO=/tmp` ⇒
`env-defaults.sh` 找不到 ⇒ 退出 1,**看着像"绿路径也退 1"**。放到仓内(临时文件、跑完即删)才是对的。
**又一次"我的验证环境和生产不同"** —— 这次差异是脚本位置。
## 三、你指出的"同一现象两种政策":加了互相指向,并写明**为什么不能统一**
- `install.sh` 的 origin WARN 旁边写明:AGC 闹钟对同一现象判**红**,**有意如此**;
- `criteria-hygiene` 第 6 条的报错里加上镜像的一句。
理由(写进两处):那边问"**本机配置能不能用**" ⇒ 离线不是配置错 ⇒ WARN;
本条问"**凭证有没有进过远端历史**" ⇒ **查不了就答不出** ⇒ 红。
并且写明统一到任一边的代价:统一到 WARN ⇒ **泄漏告警在离线时静默变绿**;
统一到红 ⇒ 离线机器上一次干跑就报假红。
## 四、残余(照实写)
- `gen:bg` 会**原地重写** `src/background-takeover.generated.css`(已跟踪)。实测内容幂等
(`git status` 干净),所以**不改内容**;但"写文件这个动作"仍在
⇒ "干跑只读"的准确说法是"**不改任何被跟踪文件的内容、不碰 dist**",不是"一个字节都不写"。
我把这句写进了注释 —— **没验到的部分不写成保证**。
- `go vet` 那条红是 `/root/.cache/go-build` **权限**问题(环境),不是代码;维持既有记录。
- 你上封提的"记录投影"已在 `406bb34` 做完;与本文无关。
- 红线仍 4 条(`narrow-layout`/`nav-merge`/`harmony-presets`/`build-stamp`),**都不是我的**。
|
2026-09-15 13:41:50 +08:00 |
|
|
|
b50a81c362
|
跨端: AGC 探针 ref 落 finally + 唯一名、披露挪到断言前;install.sh --check 不再被第一道红中止(连带修好"只在没出问题时才说话的诊断")
pi 2026-09-15 两条都成立,第二条我顺藤摸出一个**比它更大的**东西。
## 一★ origin URL 的"恢复"是**类别错误** —— 我认,而且我把它变成了可观察的
你说得对:`origin/main` 那次有真值(`ls-remote` 给的 sha),而 **URL 这个字符串没有外部真值** ——
外部只能证明它的**效果**(能到同一个仓),证明不了它的**值**。我实测两条都到 `6702cc2`,**同仓不同值**,
而我还写了"已对着远端真值恢复"。**纠正:我恢复的是"一个能用的 URL",不是"原来那个 URL"。**
按你的建议加进 `install.sh --check`(**只报值 + 判可达,不断言常量**):
```
[ OK ] 远端 origin = https://gitea.jianfgit.xyz/jianf/MailUI4Agents.git(下面是*效果*检查,值本身无外部真值可比)
[ OK ] origin 可达(`git ls-remote origin` 能拿到 refs)
```
★ **我特意没有断言它等于某个常量**:本仓**没有登记规范 URL**,写死一个就是
**"猜数字的判据"**(真值无处可得时,常量就是我的记忆)。要强判得先把规范值登记进版本库。
三条分支我都用 `set-url` 在 `/tmp` 上验过:可达 → `[ OK ]`;不可达 → `[WARN]`(**不是 FAIL**,
局域网 URL 离线时本来就不可达);未配 → `[WARN]`。
⚠️ 顺带一个**你自己也可能撞到的坑**:把 origin 指向不可达地址(`http://192.0.2.1:3999/…`)时
`git ls-remote` **挂住不返回**(我实测 60s 没回来)。"干跑卡死"比"干跑报 WARN"坏得多
(卡死的门会被当成"这脚本跑不了")⇒ 加了 `timeout 15`。**不可达必须限时。**
## 二★ 探针 ref:两处都修了,而且失败路径我用真泄露验过
按你说的照抄仓库已有先例(部署锁、`$RM` 随机名):
- **唯一名**:`refs/agentmail-probe/<ref>-${process.pid}`。
- **`try/finally`**:清理在 `finally` 里,且按**试过的每一条**删(不按"抓成功的那几条"——
`fetch` 可能**部分成功后再失败**:ref 建了、对象没齐,那一支也要删)。
- 我对后果的核实与你一致:**不是错判**(判定用 sha,对象不因 ref 被删而消失)——是垃圾/卫生。
我把它记成"我的判据去动别人的东西",而不是记成"可能报错绿"。
**失败路径实测**(不是推理):`/tmp/y` 真 bare 远端 + 真孤立泄露提交,本地**确实没有**那个对象
(先 `cat-file -e` 断言"没有",否则这次又是在验一棵有对象的树)⇒ 判据红并点名该 ref
⇒ **探针残留 0 条**。上一版我那个"失败路径测试"其实**没测到**:我用 `git clone <共享仓>`
建的 fixture **对象库就是共享仓**,`cat-file -e` 直接命中 ⇒ self-fetch 根本没跑。
**又是"验证环境与生产的关键差异恰好是 bug 所在"。**
## 三★★ 第三条是我自己撞出来的:**披露只在平安无事时发生**
你说"主动披露这个直觉是对的,缺口只在失败路径" —— 我去看代码,发现**披露那段的缺口比失败路径更大**:
```js
assert.deepEqual(published, []); // ← 判红时从这里抛出
…
if (fetched.length > 0) console.log(`(本条本次临时抓了 N 条…)`); // ← 在断言**之后**
```
即:**判红时披露永远不执行** —— 而判红恰恰是读者最需要知道"这条判据刚动过仓库"的时刻。
**我上一封说"抓过对象时必须说出来",代码实际做到的是"没出事的时候说出来"。说法与实现不一致,以实现为准。**
已把披露**挪到两条断言之前**,并且在**红路径上实测到了**:
```
# (本条本次临时抓了 1 条远端 ref 的对象:refs/tags/leak-a83e15 —— 这就是"这条判据会写仓库"的样子)
not ok 6 - ★ AGC 真身从未进过远端历史
```
## 四★★ 顺着它又挖出一个**比我那段大得多**的问题:`--check` 被第一道红中止
加完 origin 检查后我发现**它一行都没打**。插探针定位(`echo [PROBE]` 放在那一块之前,命中 **0 次**):
```
$ bash deploy/install.sh --check # 套件红(今天 3 条不是我的)
…(判据汇总打完就结束)——**那一块从未执行**
```
真因:脚本是 `set -e`,而 `npm run typecheck && npm test && npm run build` 是**第一个会红的门禁**,
**它一红脚本立刻退出 ⇒ 后面所有诊断一行都不打**。
★ **受害者不只是我新加的,还包括既有的版本库钩子检查** ——
也就是说 `install.sh --check` 那句 `[ OK ] git 钩子已接` / `[WARN] git 钩子没接`
**只在判据全绿时才会出现**。
⇒ **一个只在"没出问题"时才说话的诊断**,与"读数器替一件事作证"是同族。
(你上一轮核过 `--check` 会报钩子——那次套件是绿的,**报的是真的**;
但**这个机制比它看起来脆**:它依赖"门禁恰好全过"。)
**修法**:`--check` 下把这一门的失败**记下来**、继续跑完所有诊断,**再**传非零码出去;
**真装路径行为不变**(仍 `set -e` 立刻中止——装机器时不该带着红往下走)。实测现在:
```
[ OK ] git 钩子已接(core.hooksPath=.githooks,pre-push 会拦 AGC 真身)
[ OK ] 远端 origin = https://gitea.jianfgit.xyz/jianf/MailUI4Agents.git(…)
[ OK ] origin 可达(`git ls-remote origin` 能拿到 refs)
[FAIL] 前端门禁没过(退出码 1)—— 上面标了 [FAIL] 的那一处就是
⇒ 但**诊断已经全部跑完**(本次改动要的正是这个:别让第一道红吃掉后面的诊断)。
[FAIL] go vet / go test 不过 —— 先修好再安装
```
★ 这段的位置我改了**两次**才定:第一版放在文件末尾 ⇒ 实测 `go vet` 红(go 缓存权限)就再也走不到
⇒ **那段话本身又成了"只在一切顺利时才报的总结"**。最后放在 `go vet` **之前**,
无论后面谁红都报得出来。
## 五、我没做的 / 不在射程
- **没给 origin 断言常量**(理由见上:没有登记过的真值,写死就是猜)。
- `go vet` 红是**环境**问题(`/root/.cache/go-build` 权限),不是代码;维持既有记录,本轮不碰。
- 红线仍是 4 条(`narrow-layout`/`nav-merge`/`harmony-presets`/`build-stamp`),都不是我的。
- 上轮说的**纯函数根治**(把累加抽成纯函数、判行为不判文本)**本轮没做** —— 本轮把
"判据会写仓库"这条先按你指的两处收干净了;纯函数仍是下一轮第一件。
`ok 5`/`ok 6` 均绿、`RESULT files=25 ran=25 checks=400 pass=399 fail=1 red=4`、探针残留 0 条。
|
2026-09-15 13:26:53 +08:00 |
|
|
|
8ce1873166
|
跨端: 补上"只盖索引、盖不住 push"的那一半 —— pre-push 钩子(预防)+ 远端不变量(闹钟)
pi 2026-09-15 复核时指出我那三件照办的东西里有一条**只盖了半个风险**:
> 那条判据读的是 **索引**(`git ls-files`),它守的是"**不会被再加回来**",
> 不是"**不会被推出去**"。而 blob 现在**确实躺在两个未推送的提交里**。
**他说得对,而且这个洞的性质比"少写一条断言"严重**:我上封自己论证过
"顺序是判据的一部分:先 ignore + `rm --cached`,**再** push",
结果我把**顺序要求写进了提交信息**,没有给它任何机制 ——
而"只活在文字里的规则等于没有规则"**正是我加那条判据的理由**。同一条错我犯了两次,
一次在自己刚写完的理由上。实测确认 blob 在 `b806a05`、`6cf431e`,`origin/main` 里没有它。
## 一、补的是**预防**,不是又一条闹钟
pi 给的那行(`git log origin/main -- <路径>` 应恒空)是**健全不变量**,但它**在泄露之后才响** ——
真红的那天东西已经出去了。所以两件都做,并且**说清各自的性质**:
| | 作用 | 性质 |
|---|---|---|
| `.githooks/pre-push` | **不让它出去** | 预防(可被 `--no-verify` / 没装的机器绕过) |
| `criteria-hygiene` 新判据 | 发现"绕过发生了" | **泄露之后响的闹钟** —— 红了要按"已泄露"处置(轮换),不是"删掉再推" |
**没有把闹钟当成功劳**:判据的失败信息里第一句就写"按**已泄露**处理 —— 去 AGC **轮换**",
免得下一个人把"删掉再推"当成修好了。
### 钩子为什么放 `.githooks/`(进版本库)而不是 `.git/hooks/`(不进)
`.git/hooks/` 里的钩子**不进版本库、别人 clone 不到** —— 那还是"只在我这台机器上有效"。
`.githooks/` 进版本库,`core.hooksPath` 指过去,所以**它跟着仓库走**,换台机器装一次即可。
接线:`deploy/install.sh --git-hooks`;**`--check` 会报告它装没装** ——
理由与 `--check` 本身存在的理由同一条:**门是好的 ≠ 门接着**。
### 验证(两步,第二步才是决定性的)
1. **钩子本身**:喂它"会泄露的范围"⇒ 输出点名两个提交、**退出码 1**;
喂"已推过的范围"⇒ **退出码 0**。(退出码**不经管道取值** —— 管道会吃掉它,这个坑我踩过。)
2. ★ **git 真的会调它吗**:`git push` 到一个**真的 bare 仓**(`/tmp/bare-test`)⇒
钩子跑起来、**推送被中止**、`git --git-dir=/tmp/bare-test log` 报**空仓**(什么都没过去)。
—— 只做第 1 步的话,"钩子写对了但没接上"这个形状**分不出来**。
## 二、判据的**两半**都做了变异验证
这条判据**现在本来就该是绿的**,所以"跑绿"什么也证明不了(**一条永远为空的假判据也会绿**):
- 把 `origin/main` 指到一棵**含该文件**的树上 ⇒ **红**(真泄露它会响);
- 把它的探针改坏(`git log -- <不存在的路径>`)⇒ **红**("空"与"探针坏"分得开)。
第二半是特意加的:否则这条判据的"绿"可能只表示**查法不管用**。
判据开头那条"本地历史里**确实**有它"的自检,就是为了让这个区分成立。
## 三、★ 我在验证过程中**弄坏了共享仓库的一个 ref**,已按远端真值恢复
得说清楚,不能只报"验证通过":我的变异脚本直接改 `refs/remotes/origin/main`,
而 `restore_ref()` 用的是 `git update-ref -d` + `remote remove` —— **对 remote-tracking ref 根本不还原**。
于是我一度把 `origin/main` 指在了自己伪造的提交上(`2b04319`、`201a668`)。
**这是共享工作树,别的会话会读这个 ref。**
恢复方式**不是**凭记忆写一个 SHA,而是**直接问远端**:
```
$ git ls-remote origin refs/heads/main
6702cc2f5e refs/heads/main
```
现在 `rev-parse refs/remotes/origin/main` 与该值一致,`behind=0 ahead=130`(与我改动前一致)。
**教训**:验证"推送前"这类行为时,**必须用一次性的 bare 仓做目标**(我第二次就是这么做的),
**不能改共享的 ref**。造出来的东西要能被**外部真值**核对 —— 否则"恢复"也只是我自己的另一个断言。
## 四、pi 替我跑的 Go 测试
`cd server && go test ./internal/repo/` → **ok,1.236s**(带着我新加的那笔,`docs/DEBTS.json` 现 15 笔)。
所以我那句"读代码得出的结论、标未验"**可以升级成已验证** —— 谢谢,这条我之前确实跑不了。
## 五、未做
- 到期那 7 条(`STATIC_ONLY`)仍未动 —— 独立工作。
- `RESULT` 汇总行的 `fail=1` 与"红的判据(4/25)"口径不一致,**仍未查**(上封已提及,本轮先记着)。
- 红仍是 4 条,**都不是我的**(`narrow-layout` 88>64、`nav-merge` 9>8、`harmony-presets` 6>5、`build-stamp`)。
本轮 checks 790 → 792(我加的两条子测试)。
|
2026-09-15 12:41:05 +08:00 |
|
|
|
46fa7fa729
|
feat(push): 可选、配置式、多厂商的推送通道(HMS 为首个实现)
用户要求:推送密钥必须是可选项(自部署后端不能写死推送方式),且要支持
多厂商配置式接入 —— 每个用户各自部署服务器、自己选厂商、自己配凭证。
所以落地成:
· internal/push:通道抽象 + 工厂表(RegisterType),加厂商不改配置层与端点形状;
HMS 只是第一个实现(internal/push/hms.go)
· 配置在 PUSH_CONFIG(默认 <AGENTMAIL_DATA_DIR>/push.json),一项一个厂商,
凭证走文件(app_secret_file / files.*,建议 600);环境变量只是可选覆盖
· 没配 = 整条推送路径连一次查库都不发生(shouldDispatch 早退);
单项配错(未知类型/密钥读不到/enabled:false)只跳过那一条,不影响启动
· push_tokens 表带 provider 维度 + 三个 /me/devices/push-token 端点;
没配推送时端点照存并回 enabled:false(登记成功 != 服务端开了推送)
· notify.Recipients 末尾异步挂钩:收件人名单直接用 SSE 那份 seen(两条通道
共用同一份"谁该收到"的判据);失败只记日志,绝不拖住收信
HMS 的形状是拿真凭证打线上接口问出来的(v1 + message.token[] + testMessage;
payload/target 形状 v1 不认、v2 要服务账号 JWT)。未上架应用必须 test_message=true,
单批 ≤10 token(MaxTokensPerRequest 声明)、每日 1000 条兜底(项目级额度)。
实测:App ID + App Secret 能换到 access_token(3600s);形状被线上服务接受。
判据:repo 6 条 + push 12 条 + handler 3 组,全部做过**变异验证** ——
过程中抓出两条假判据(异步分发与 t.Cleanup 赛跑而假绿;密钥文件优先级没被覆盖)
并补掉。Go 全量测试与 go vet 干净。
★ 未验:端到端真机送达(需要真机 token + 客户端按 com.jianf.agentmail 重编并签名,
签名指纹还要在 AGC 登记)—— 从未真正发出过一条能到达设备的推送。
详见 docs/HMS-PUSH-PLAN.md 的「实现状态」一节。
|
2026-09-15 11:21:00 +08:00 |
|
|
|
b7dc9e90e6
|
fix(deploy): 权限政策按**文件类型分岔**开药方 —— 我上一版把 release-linux.sh 的执行位改掉了
## 我在上一个提交里弄坏了一个脚本
`client/electron/scripts/release-linux.sh` 原本是 **711**:
group/other 读位缺失 ⇒ 本判据判红(**判得对**),但我给的药方是无脑 `chmod 644`
⇒ **去掉执行位** —— 把"读位缺失"换成"不能执行",**比原来更坏**,
而判据随后报"通过",工作区显示这个文件被改(`M`)。是我在提交前扫 `git status` 时
看见那一行 `M client/electron/scripts/release-linux.sh` 才发现的 —— **不是判据发现的**。
已恢复 755(`git diff` 干净,因为它 git 记录是 100755)。
★ 教训:**一条判据说"这个文件被改错了"时,药方必须按文件类型分岔**;
只有一个药方(`chmod 644`)就会把另一类文件改坏,而它同样报"已修好"。
这与"判据的适用范围没写出来"同族,只是这次代价落在了**修的动作**上,不是读的动作上。
## 改法
可执行与否按 **git 记录的那个位**(`git ls-files -s` 的 `100755`)判,
不按当前文件系统权限 —— 否则"执行位被谁弄丢了"会被这条判据**追认**成正常。
三条区分力实测(都当场恢复 + `cmp` 校验):
1. 非可执行文件改 0600 ⇒ 红,文案"权限过严";
2. 可执行文件丢掉执行位 ⇒ 红,文案"**注意别去掉执行位**";
3. 可执行文件 711(= `release-linux.sh` 原来的形态)⇒ 红 —— 即**最初那次判红是对的**,
错的是药方。
另外把计数循环改成读 `git ls-files -s` 一次(不再在循环里对每个文件调
`git ls-files --error-unmatch`,也不再让计数落在管道子 shell 里)。
验证:`check-file-modes.sh` 正常态 exit 0;三条变异行为如上;
`git status` 只剩本文件本身的改动。
|
2026-09-15 10:27:20 +08:00 |
|
|
|
15df621045
|
feat(deploy): 权限位两条判据 —— 政策(源文件不严于 0644)+ 一致性(副本=仓库)
pi 评审 2026-09-15 §三 给了决定:**开,但拆两条**。理由我采纳并写进注释:
合成的结果会是"看起来覆盖了、其实只覆盖一半"。
## 为什么要这两条(实测实例,不是设想)
写文件的工具**不理会 umask**(umask 022,它建的仍是 0600),而
`deploy/redeploy-plugin.sh` 用 `cp -a "$SRC/."` 打快照 ⇒ **0600 会进生产**。
实测:快照里 `src/paths.mjs`/`src/turn-cwd.mjs` 是 0600、仓库 0644,
而 **`cmp` 五个 same、① 报"逐字节一致"** —— 两头都不报警:
`collectFiles` 只把**内容**做 sha256,`check-deploy-drift` 只在脚本上判可执行位。
## 两条的分工(别合成一条)
- `deploy/check-file-modes.sh` = **政策**:源文件不得比 0644 更严。
**不看快照** ⇒ 能抓"两边都 0600",而一致性那条永远抓不到(两边一致 ⇒ 恒绿)。
- `check-deploy-drift.mjs` 新增 **①b**(`collectModes`/`diffModes`)= **一致性**:
部署副本的权限 = 仓库那一份。抓不到"两边都错"。
## 落地
**政策侧**:新脚本判「已跟踪文件里 group/other 任一读位缺失」。
实测判出 **53 个**(含 `plugins/pi-mail-bridge/package.json`、`src/naming.mjs`、
`client/harmony/.../*.ets`、`docs/GUI-PLAN-HARMONY.md` 等)—— 全部 `chmod 644` 修掉,
现在 exit 0。**区分力实测**:把 `deploy/check-shared-libs.sh` 临时改 0600 ⇒ 判据红并点名它,
恢复后绿;0755 的可执行脚本**不**被判红(有读位,不是"更严")。
★ 也修正了我先前的一处过报:那 53 个里有凭据类命名的文件吗 —— **0 个**(先查了才批量改)。
**一致性侧**:①b 一上线就抓到**真实的**、**先于本次改动**存在的漂移:
`plugins/*-mail-bridge/lib/permission-grants.{js,d.ts}`、`rename-proposal.{js,d.ts}`
在三个宿主的快照里是 **600**、仓库是 **644**(pi 宿主 1 处、dsh 4 处、opencode 2 处)。
⇒ 这条缝**一直存在**,只是此前没有任何判据看着它。
**处理**:不单独 redeploy 去"洗"权限(那要重启 pi 宿主,为权限位重启服务不值得),
**下一次正常部署顺带修好** —— 这是记账,不是新欠账。
①b 现在是**失败**态(真实不一致),所以 `drift` 整表 exit 1;这与"① 全过"并存是对的,
两条量的是不同的东西。
## 自检
给 `collectModes`/`diffModes` 加了 4 条自检(`--self-check`),四条一起写,
因为"能发现差异"单独一条会被一个**恒判"都不同"**的坏实现骗过:
① 权限相同不得误报;② 内容一致但 0600 vs 0644 必须被发现(连数值一起断言);
③ 权限差异不污染 ① 的内容判据;④ 仅一侧存在的文件不算权限漂移(归 ① 的文件集判据)。
★ 其中两条我**第一版写错了**并当场修掉,都记在注释里:
- 用了 `lib/x.mjs` 做样本,而上面 `mk(b,'DIFFERENT')` 已把它改成内容不同
⇒ ③ 红在**内容**上,而它想验的是"权限不污染内容";改用一对独立的内容相同文件。
- 夹具真实创建的是 `lib/x.mjs` 而不是我以为的 `lib/same.mjs`(ENOENT 才发现)。
验证:`--self-check` 全绿;pi 桥 509/509;`check-shared-libs` exit 0;
`check-file-modes.sh` exit 0;`install.sh --check` exit 0。
|
2026-09-15 10:26:23 +08:00 |
|
|
|
be8459cfe7
|
fix(deploy): 环境兜底自己依赖的命令也登记 + 自我检查排在用它们之前 + 静默改 HOME 必须留痕
pi 评审 2026-09-15 报的"第五次环境假设",在 `env-defaults.sh` **自己**身上。
他指出的**结构**成立:本文件用了 `id`/`getent`/`cut`/`df`/`awk`,一个都没登记进
`AGENTMAIL_REQUIRE`(那张表只登记**调用者**的命令,且由调用者在**source 之后**赋值)。
★ 但我实测发现**他给的两个具体后果在这台机器上不可达**,原因值得记下来:
`env-defaults.sh` 的 ④ PATH 自修(`:46`)在 PATH 里没有 `/usr/bin` 时会**把它加回来**
⇒ "从 PATH 里拿掉 id/getent/cut/df/awk"这种造法**必然被自修抵消**(我第一版探针就栽在这里:
`id -u` 根本没失败,我却按"失败了"往下推理,直到把 `command -v id` 单独打出来才看见)。
缺这些命令只可能发生在"**`/usr/bin` 里真没有它**"的机器上(distroless / 精简容器)。
所以这次修的是**能 durable 判定的三件**,而不是他描述的失败面:
1. **登记**:新增文件级常量 `AGENTMAIL_REQUIRE_SELF="id getent cut df awk"`。
为什么不写进三个调用者的 `AGENTMAIL_REQUIRE`:那个变量在 source 时**还不存在**
(`. env-defaults.sh` 在第 16/42/52 行,`AGENTMAIL_REQUIRE=` 在第 20/46/56 行),
本文件没法把它自己那份追加进一个"稍后才被赋值"的变量 —— 追加了本次也不生效。
2. **自我检查排在用它们之前**(顺序即正确性,同 ①→④ 那条):新增 ③b-0 段,
只用了**内建命令**(`command -v` + `printf`),所以能在"环境还什么都没兜"时跑;
它现在位于 `:76`,而第一次真正用这些命令的 `id -u` 在 `:102`。
⇒ 缺 `df`/`awk` 时**不再静默丢门**:原来 `df -Pk … | awk` 拿到空串会落进
`''|*[!0-9]*)` 那支"读不到 ⇒ 不判定",**②b 那道空间门直接消失**(那是门,不是提示)。
3. **静默改 HOME 必须留痕**:原先只在"**调用者给的** HOME 不可写"时 WARN,
而"按身份推出来的那个也不可用"(root 的 `/root` 在非 root 下不可写;
passwd 里是 `/nonexistent`)**悄悄换了 HOME** —— 与本文件存在的理由正好相反。
现在两条路都 WARN。★ 这一条**可达且实测过**:
`setpriv --reuid=65534 … bash -c 'unset HOME; source env-defaults.sh'`
⇒ `[WARN] 按身份推出来的 HOME=/nonexistent 不可用 … 改判到 /tmp/agentmail-home-65534`。
**判据 4 条**(`test/env-guard.test.mjs`,pi 桥侧,与该文件既有的环境判据同处):
① `AGENTMAIL_REQUIRE_SELF` 登记了这 5 个命令;② **顺序**:自我检查的行号必须**小于**
`id -u` 的行号(判据写成位置比较,而不是"有这段代码" —— 后者正是我这一轮反复写坏的形状);
③ 源码里存在"按身份推出来的 HOME 不可用"那句 WARN;④ **端到端**:非 root + 空 HOME
真的打出 WARN。
★ 这条端到端判据我写坏了**两次**,都记在文件里:
· 第一版用 `execFileSync` 只收 stdout,而 WARN 走 **stderr** ⇒ 红在"没找到 WARN"上,
实际是**判据自己没读那一股**;
· 改用 `spawnSync` 后仍红 —— 因为 `deploy/lib/env-defaults.sh` 是 **0600**,
`nobody` 读不到它,脚本**压根没跑起来**。这与"命令不在 ≠ 输出为空"是同族:
**脚本没跑 ≠ 输出里没有那一行**。判据改为用一份世界可读的副本(文件权限是另一件事)。
⇒ 顺带发现并修掉:我用写文件工具建的 5 个文件都是 **0600**(该工具不理会 umask),
已全部改 644(仓库既有约定;同目录其他文件都是 644/755)。
**`cp -a` 会把 0600 带进生产快照**,所以这不是纯本地问题 —— 记一笔,未另开检查
(工作区里还有 52 个 git 已跟踪文件是 0600,是既有状态、非本次引入,单独处理)。
验证:pi 桥 **509/509**(+4);`check-shared-libs` exit 0;`install.sh --check` exit 0。
|
2026-09-15 07:05:50 +08:00 |
|
|
|
18c9aef219
|
chore(deploy): 会话归档脚本(判据 / 备份 / 回滚真跑一遍)—— 用户「清理过时的请求与邮件」
判据:active 且 > IDLE_HOURS 小时无动静,KEEP_IDS 保护人的在用来话。
归档(不删、不碰 updated_at);备份 .backup + integrity;id 清单落文件。
回滚脚本生成用**带引号 heredoc**,且把回滚在**副本库上真跑一遍**才算有回滚路径 ——
第一版用 awk printf 拼 IN 列表拼出 \"''id2\"(引号错位),判据当场抓住(rc=0 但
active 没回到 16);改成 bash 循环拼后副本库实测 12 条复活、active 回到 16。
本次落地:16 → 4 条 active,12 条过时会话(349 封信、76 条权限问询)离开收件箱。
|
2026-09-15 07:00:35 +08:00 |
|
|
|
1f48c5c4ff
|
feat(sandbox): am-sandbox —— 给命令套 Landlock 内核边界(界内可写、界外 EACCES)
「工作区档」此前名不副实:档位表写「本目录内可动、越界要问人」,而 pi 这一路只能按
**工具名**判(bash/write/edit 一律问人),因为命令的影响范围无法从文本静态判定
(`cd /工作区 && rm -rf /opt/x` 以"进工作区"开头)。pi 自己**故意**不内置沙箱
(docs/security.md §No Built-in Sandbox:进程内的部分沙箱会被误解成安全边界,
"Real isolation needs to come from the operating system or a container boundary")——
dsh 是把沙箱放进自己的运行时;这个二进制补的是 pi 这一侧的**宿主**部分。
## 语义
写:只有 `--rw` 列出的目录(含子树)与 `--rw-file` 列出的文件可写,其余写操作一律
EACCES(内核判)。读与执行不限制 —— Landlock 只做白名单式加法,要连读都挡住得靠
容器/挂载命名空间。套不上边界时 **fail closed**(退出码 126,不执行命令):静默裸跑
会让"档位=workspace"变成谎话,而谎话比做不到更危险。
## 判据
- 行为 `deploy/check-sandbox.sh`:19 项全绿 —— 界内可写/子目录递归/rename+删除、
读界外、执行、写 /dev/null;界外新建/建目录/覆盖/删除/删目录/跨边界 rename/
软链接逃逸/子进程继承;**对照**(不套边界时那些"必须被拒"的命令必须成功,否则
判据可能只是环境本来就只读);fail-closed 那条(rw 不存在 ⇒ 126 且命令未执行)。
- 算法 `cmd/am-sandbox` 单测:按 ABI 逐位裁剪、文件目标不得带目录类权限、
allowed ⊂ handled、参数解析。
## 过程中撞到两个"看着能过"的坑
1. `--rw-file` 任意文件 → add_rule **EINVAL**:文件目标只能用文件类权限
(MAKE_*/REMOVE_*/REFER 是目录类),而错误信息只有 "invalid argument",
看不出是权限位不匹配。单测里"文件权限必须是 handled 的子集"就是拦它的。
2. ★ 判据自己翻车:`"$SB" … 2>&1 | grep -q "Permission denied"` 在
`set -o pipefail` 下 —— **grep 命中即退出 → 左侧 EPIPE 失败 → 整条管道判失败**
⇒ 7 条"界外必须被拒"全被判成"没被拒"。改成先收输出再匹配,顺带打印真实输出。
## 已知边界(写进文件头 + 判据输出留痕,不假装没有)
- 元数据(chmod/chown/utimes)不受 Landlock 管辖:界外文件**内容**改不了,
但**模式位**能改
- 网络出向不受限
- 界内**已存在**的、指向界外文件的硬链,经界内路径写入会改到界外(路径式沙箱固有)
- 本机 ABI=2(无 TRUNCATE);实测 `truncate` 越界仍被拒(coreutils 先 open 写 ⇒
WRITE_FILE 挡住),所以那个缺口比纸面上窄
- **读不限制** ⇒ 以 root 跑的 agent 仍能读整机(含 /etc/agentmail/*.env)。沙箱在这里
的价值是"界外留不下脚印",不是"拿不到东西" —— 要后者得上容器
## 接线
`redeploy-gateway.sh` / `install.sh` 在**边界判据真跑绿了**之后才装到
`/opt/agentmail/bin/am-sandbox`(装一个套不上边界的工具等于发空头支票)。
pi 桥的接线(按档位把 worker 套进边界)还没做 —— 见随后的报告。
|
2026-09-14 23:33:11 +08:00 |
|
|
|
4c2bf26c42
|
fix(deploy): flock 没登记进 AGENTMAIL_REQUIRE("缺命令"被报成"另一个部署在跑")+ 中断 trap + 两条欠账入册
**1. 自指缺口:新能力带的新依赖没登记回表(pi 抓到)**
我加"同时性"那一列时引入了 `flock`,**却没把 `flock` 加进三个脚本的 `AGENTMAIL_REQUIRE`**。
后果实测:
PATH 里没有 flock ⇒ `flock: command not found`(127)⇒ `! flock` 为真
⇒ 打印"**另一个部署正在跑(锁被占用)**"
退出码事后是对的(2),但**诊断是错的** —— 而照着它做的是"等另一个部署结束":**永远等不到**。
三个脚本各加一个词;并在 `env-defaults.sh` 的 ③b 注释里写明这条规矩
(**新增任何外部命令时回到 `AGENTMAIL_REQUIRE` 登记**)与这个实例。
→ docs 第 19 条:「表与被表的东西不同步」。
**2. 第六列候选:中断(信号)—— 已按 pi 的建议修 `redeploy-gateway.sh`**
原子 `mv` 修的是"半截二进制",**没修"服务停着而脚本死了"**:
第 250 行 stop 与第 267 行 start 之间被外部信号打断(Ctrl-C、宿主杀进程、会话回收、OOM)
⇒ 脚本直接退出、**服务留在停止状态而什么也不说** ⇒ "邮件全停 + 无人告知"。
已加 `trap … INT TERM HUP`:进窗口前置位 `_SERVICE_STOPPED`,出窗口复位并摘 trap;
**trap 只在"确实还停着"时才动手**(否则会多起一次服务);回滚分支也维护该标志。
**用 stub `systemctl` + 探针真喂过四个分支**:
stopped=1 + SIGINT ⇒ 调了 `systemctl start`、退出码 130、打印点名
stopped=0 + SIGINT ⇒ **没有**调用 start(不误起)
(探针里两次 harness 自身的错也一并记下:`sed`/`awk` 的区间端点选错,
把 `trap -` 也取进来,导致"trap 没生效"的假象 —— 是探针错,不是代码错。)
★ 顺带修掉自己写的一处:`printf '… $SERVICE …'` 用**单引号**包裹 ⇒ `$SERVICE` **不展开**,
原样打出字面量(探针里实测看到)。改双引号传参。这类"消息里有变量但没展开"会让读者
以为服务名真叫 `$SERVICE`。
**3. `install -d -m` 对已存在目录的行为:实测会改(pi 的疑问)**
mkdir -p 建 755 → `install -d -m 0700 <同一目录>` → **700**
所以**下一次部署就会收紧** `/opt/agentmail/data` 与 `/etc/agentmail`,不需要额外的
`chmod 0700` 动作,也不必为此单开一次"人按一下"。
(我按这条如实回报,因为 pi 说过"若不会改就需要显式 chmod,且安全意义比
`user-question.js` 高" —— 结论是不需要。)
**4. 两条欠账入 `docs/DEBTS.json`(按 pi 的界线:只修新机制自己引入且会误报的缺口)**
· `deploy-space-prefix-fs`:空间列只铺了 `$TMPDIR`,没铺 `$PREFIX` 所在文件系统
(属"列内没铺满",不是新列)。
· `deploy-interrupt-trap-other-scripts`:trap 只在 `redeploy-gateway.sh`;
`install.sh`/`redeploy-plugin.sh` 被打断同样会留半成品(没有"服务停着"那种后果,故低优先)。
→ docs 第 20 条同时记下 trap 这条纪律与它的可喂判据写法。
验证:install.sh --check exit 0;npm test exit 0;prune 自检 22/22;drift 自检 35/0;
check-shared-libs exit 0;全部 deploy 脚本 bash -n 通过;DEBTS.json 有效(13 条)。
|
2026-09-14 21:26:51 +08:00 |
|
|
|
1056b22cbd
|
fix(deploy)!: install 不是 rename(头部那句"原子"论断不成立,实测半截二进制)+ 加并发锁 + journalctl 抽成可喂函数 + data/ 权限
pi 的四条,逐条实测:
**1. `install(1)` 不是 rename —— 而那句话是整节设计的理由**
头部原话:"install(1) 本质是 rename,是原子的 —— 要么完整换掉,要么原样不动"。
按他给的命令实测 `strace … install -m 0755 /bin/true /tmp/t`:
目标不存在:`openat(t, O_WRONLY|O_CREAT|O_EXCL)`
目标已存在:`unlinkat(t, 0)` → `openat(… O_CREAT|O_EXCL)` → 写入
**全程没有 rename/renameat**。即复制路径,**旧文件在新文件写完整之前就没了**。
中途失败实证:`ulimit -f 1` ⇒ 退出码 **153**(SIGXFSZ),目标变成 **1024 字节截断 ELF**,
原 14 字节内容**已被销毁** —— 正是本段前半句写的风险,`install` 并不免疫。
(第一次测时我把退出码经管道取到了 `head` 的 0 —— 正是 docs 第 6 条那个坑,重测才拿到 153。)
★ 他补的第二个坑也确认:`/tmp` 与 `/opt/agentmail` **不同文件系统**
(实测设备号 40 vs 2049)⇒ 就算换成 `mv` 也不原子(跨 fs 退化成 copy+unlink)。
已改成真原子三步:**目标同目录**暂存 → `install`(动的是"还没人用的名字")→ 一次 `mv -f`。
对照 `redeploy-plugin.sh` 的 `mv "$STAGING" "$SNAP"` 是**真原子**(同 fs)——
同一仓库原先两套"原子切换",一套真、一套名义上的。
**2. 缺并发锁(环境前提表的第五列:同时性)**
原表(变量/命令/空间/身份)漏了这一类,而它不是假设:工作区是多 agent 共用的。
两个部署同时跑 ⇒ 各自 stop(一次失败、状态没人看)→ 两次写同一目标(配合上面那条 ⇒
真能留半截)→ 两次后置验证互相把对方的"验证不过"当自己结论 → 谁回滚不确定。
三个脚本都加 `flock`(**不是**"检查锁文件存在",那本身有竞态)。
实测:同一把锁上第二个进程 `flock -n` 失败;脚本形态下 `install.sh` 的 `--check` 不建锁
(干跑只读、且刻意允许无写权限运行)。
**3. journalctl 抽成可喂函数 —— 并且他对我那次"复现"的更正成立**
他说我复现的是**"空输出"支**,不是"读不到"支。实测确认:
`journalctl -u 不存在的-unit` 退出码 **0** ⇒ 我测到的是 else 分支。
已抽成 `am_scan_logs <unit> <since> <pattern> [命令]`,输出三态
`clean`/`hit`/`unreadable:<码>`(与既有 `describeEnvError`、`judgeRestart` 同一做法:
把能被样本喂的部分抽出来)。**用 `/bin/false`、`/bin/true`、假"输出含 panic"的脚本
三个样本喂过**(不碰生产):`unreadable:1` / `clean` / `hit` —— 三条支路现在都有覆盖。
判据也随之分开:**"命令不在"由 `AGENTMAIL_REQUIRE` 兜、"命令在但读不到"由这个函数兜**,
两列在代码里分开,而不只是注释里分开。
**4. 低优先项里 umask 那条是真问题(我原来以为可忽略)**
实测 `install -d` 权限位受 umask 影响;而本机生产 `/opt/agentmail/data` = **755**、
`agentmail.db` = **644**(全局可读),同一脚本里 `agent-config`/`pi-config` 却是显式 `-m 0700`
—— 同一脚本两套口径,而那个库里是全部往来邮件。
已改:`install -d -m 0700 "$PREFIX/data"`、`-m 0700 "$ETC"`(密码与密钥)。
(现有生产权限不在本次改动范围,属部署后生效。)
**顺带修一处我自己的口径不一致**:`install.sh` 的 root 检查用 `exit 1`(判据失败),
而另外两个脚本与 `env-defaults.sh` 的"环境不足"都用 **2** —— 调用者无法据此区分
"该重跑"还是"该修代码"。已统一为 2。
★ 加锁过程中我自己连踩三次"复制粘贴的上下文假设"(都已修,并记进 docs 第 18 条):
`install.sh` 没有 `bad()` ⇒ 127;`redeploy-plugin.sh` 没有 `$PREFIX`(用 `$DEST_ROOT`)⇒
`unbound variable`;`install.sh --check` 无写权限 ⇒ 建锁 `Permission denied` 又变 127。
**同一份代码搬到另一个脚本里,能引用的变量和函数是不一样的。**
验证:install.sh --check exit 0;npm test exit 0;prune 自检 22/22;drift 自检 35/0;
check-shared-libs exit 0;全部 deploy 脚本 bash -n 通过。
|
2026-09-14 21:19:46 +08:00 |
|
|
|
8e3b04a267
|
fix(deploy): TMPDIR 只判"未设"(同文件里 HOME 判了可写)+ 环境自足漏了"命令"(journalctl 两处是假绿)
pi 给了"第五次"的两条线索,都在我读得到的地方,逐条实测确认后修完:
**1. TMPDIR 与 HOME 不同规则(就在同一个文件里)**
① `HOME` 那边写了两条规则:`mkdir -p` 对**已存在的不可写目录会返回成功** ⇒ 必须单独判 `-w`;
判据落在"能不能写"不落在"路径像不像"。**同一条规则没落到 ② `TMPDIR` 上** ——
而 ENOSPC 正是这条链的元老问题(四次史里第 3 条就是 TMPDIR)。两种失败形状:
已给但**不可写**(EACCES)、可写但**已满**(`-w` 抓不到,要的是**空间**判定)。
已补 `-d` + `-w` + 可用空间(`df -Pk`,读不到⇒**不据此判定**;`0` 是**真的没有**);
不足 ⇒ 人话 + exit 2。**不 import** 插件那份 `test/lib/tmp-space.mjs`:
`deploy/` 侧要能独立分发,为去重引进平台代码不划算(按既定理由,写最小版本)。
实测 `TMPDIR=/root/nope` ⇒ `[FAIL] 环境不足:TMPDIR=… 不存在或不可写` + 退出码 2。
**2. 环境自足只覆盖"变量",没覆盖"命令" —— 其中 journalctl 两处是假绿**
这节的要害是 pi 给的那句判据,我认:**"命令不在"必须走 2/红 + 人话;
"命令在但输出为空"才是判定结果。** 原先两处把两者压成同一个字符串 `"0"`:
journalctl 失败(被 `2>/dev/null` 吞掉)⇒ grep 读空 ⇒ `fc="0"` ⇒ **打印"无 panic/fatal"**。
实测复现:`journalctl -u 不存在的-unit | grep -icE 'panic'` ⇒ `fc=[0]`。
`sse` 那条同形、后果更坏:**把"读不到日志"归因成"插件没连上"**,让人去查密钥。
⚠️ 顺带实测:**`PIPESTATUS` 分不开这两种情况**(命令不存在与"存在但无匹配"都给 1),
所以不能靠管道状态区分 —— 必须**先取输出、成功后再过滤**,命令存在性另做前提检查。
改法:两处都改成"先取日志、看退出码";读不到 ⇒ `warn` 明说"读不到、无法据此判断"
(既不假绿也不假红)。并给三个脚本加 `AGENTMAIL_REQUIRE` 前提检查
(缺一个 ⇒ exit 2 + 人话),与四次史的处理**同形**,只是对象从变量换成命令。
实测:`AGENTMAIL_REQUIRE` 里放不存在的命令 ⇒ 退出码 2。
**3. 顺带修 pi 点到的两处同族问题**
· `install.sh` 的 `HEAD_REV="$(git … rev-parse --short HEAD)"`:`set -e` 下失败**直接中止**
(实测退出码 127、无翻译);而且 HEAD_REV 为空会让下一句报
"这个包比源码旧:产物 gitRev=… ≠ HEAD=" —— **把"这里不是 git 仓库"说成"产物过期"**。
已改成显式判失败 + 明说"读不到当前 HEAD,跳过新旧比对"。
· 同块第 94 行末尾挂着一个 `|| true` ⇒ 整行退出码恒 0 ⇒ 它作为 `if` 条件**永远为真**
("判据的形式在、区分力不在")。已改成显式计算、去掉 `|| true`。
docs 补两条纪律:15「"命令不在" ≠ "命令在但输出为空"」(含 PIPESTATUS 分不开的实测)、
16「一条规则写了,要检查它是否落到了所有同类对象上」。
验证:install.sh --check 空环境 exit 0、正常 exit 0;TMPDIR 不可写 exit 2;
npm test exit 0;prune 自检 22/22;drift 自检 35/0;check-shared-libs exit 0。
|
2026-09-14 21:08:48 +08:00 |
|
|
|
3be824849d
|
fix(deploy): env-defaults 的顺序错(③ 的探测依赖 ④ 的产物)+ 非 root 的 HOME 陷阱 + 兜底行可见 + 补 root 断言
pi 读了新加的 `deploy/lib/env-defaults.sh`(三个 source 点他都确认对),指出三条,逐一实测后处理:
**1. 顺序错(真错,而且正好落在它自己要消的那类假设上)**
③ 用 `command -v go` 探测,而 `command -v` **走 PATH**;④ 才修 PATH ⇒
**在 ④ 要修的那个环境里(PATH 为空),③ 的探测必然失败**,紧接着 ④ 把 PATH 装上、
后面的步骤**又能**找到 go —— **探测结论与实际可用性相反**。
实测(`env -i`):顺序翻转前 `go` 在 ③ 处找不到、④ 之后 `/usr/bin/go` 就在了。
已把 PATH 那段**挪到最前**,并在文件头写明顺序是**正确性而不是风格**。
(pi 自己也说了严重度:今天对 go 大概率无影响,因为 ① 已兜住 HOME、现代 go 会从 `$HOME/go`
自推缓存 —— 他把它当形状问题提,这个判断我认;一条探测所依赖的东西正是同文件后面要修的东西,
这正是本文件存在的理由。)
**2. `HOME=/root` 在非 root 调用者手里会把环境问题变成代码问题**
`redeploy-gateway.sh` 原先没有 EUID 断言(只有 `install.sh` 有),于是"非 root + 空 HOME"
会拿到 `HOME=/root`,接着 `go build`/`npm ci` 往 `/root/go`、`/root/.npm` 写 ⇒ **EACCES**,
而那串报错看起来是代码/工程问题 —— 正是本文件要消的东西。
已按身份分叉 + **验证可写**(判据落在"能不能写",不落在"路径长得像不像"),
兜底落到 `${TMPDIR:-/tmp}/agentmail-home-$(id -u)`;连一处可写的都找不到 ⇒ exit 2 + 人话。
★ **顺着他的思路又实测出第二个口子**:`HOME` **已给**但不可写时,上面只判"未设"就放行 ——
后果与空 HOME 完全相同,只是入参不同(`sudo -E`、从 root shell 继承、容器挂错)。
`mkdir -p` 对**已存在的不可写目录会返回成功**,所以必须单独判 `-w`。
实测 `setpriv --reuid=65534 env -i HOME=/root` ⇒ `touch $HOME/probe` 被拒。
已覆盖"已给但不可写",并**先说清再改判**(`[WARN] 调用者给的 HOME=… 不可写;改判到 …`),
不静默换目录 —— 静默换会让"东西写到哪去了"变成谜。
五种场景实测(全空 / 环境齐 / 非 root+空 / 非 root+不可写 HOME / root+可写):全部符合预期。
**3. "某条脚本忘了 source"没有信号** ⇒ 采纳
`AGENTMAIL_ENV_DEFAULTS` 只是被 export、值不进正常输出 ⇒ 谁把 `source` 删了,
输出与"环境本来就齐"**完全同形**(又是"看起来在兜、其实没兜")。
新增 `agentmail_env_report()`,三个脚本各自打一行(兜了哪些 / "(无 —— 调用者已提供全部)";
忘了 source 就没有这一行)。实测三个脚本在空环境下各自都打出来了 ——
这也把验收从"一条脚本"变成"三条各自可读"。
**4. 顺带补 `redeploy-gateway.sh` 的 root 断言**
它要往 `$PREFIX`(默认 /opt/agentmail)写,非 root 必然失败在写权限上,
而报错来自 `install`/`cp`、看起来像工程问题。用退出码 **2**(环境/权限),口径与 env-defaults 一致。
实测非 root 下:`[FAIL] 环境不足:本脚本要写 /opt/agentmail,需要 root。` 退出码 2。
验证:install.sh --check 空环境 exit 0、正常环境 exit 0;npm test exit 0;
prune 自检 22/22;drift 自检 35/0;check-shared-libs exit 0;四个脚本 bash -n 通过。
|
2026-09-14 21:01:19 +08:00 |
|
|
|
743e397916
|
fix(prune)!: 构建暂存那段清理**一直是空转的**(ls -1t 对目录打 路径: 头)+ del() 失败分支补齐覆盖
## 那个真 bug:`ls -1t <多个目录>` 不打裸名字
追 pi 的 shim 建议时撞出来的,与 shim 无关 —— 是我为了给它造样本才发现的:
ls -1t /tmp/agentmail-gateway-build-* # 这些是**目录**(mktemp -d 造的)
/tmp/…-20260101-000000:
agentmail-gateway
/tmp/…-20260105-000000:
agentmail-gateway
`ls -1t` 收到**多个目录参数**时会列出**每个目录的内容**并打 `路径:` 头 ——
于是 `mapfile` 拿到的全是 `…000000:` / `agentmail-gateway` 这类行,都不是文件名
⇒ `basename | grep -oE '[0-9]{8}-[0-9]{6}'` 取不到时间戳 ⇒ 每条都判
"文件名无时间戳,判定不了" ⇒ **一个都不删**。
也就是说本段注释里写的那个问题("每次部署留下一个 24MB")**从来没被清理过**。
修法:`ls -1dt`(`-d` 让目录自身作为条目,不打头)。
**为什么一直没人发现**:自检夹具用 `: >` 造的是**普通文件**,而生产是**有内容的目录**。
夹具形状与生产不一致 ⇒ 夹具自己认了错形状,而判据 197 行又只按**文件名**判
"窗口内的还在、窗口外的不在",于是判据也认了。这是 docs 第 13 条那一族。
→ 夹具已改成真目录 + 里面放 `agentmail-gateway`;**改完立刻变红**("干净样本:/tmp 构建暂存
只留窗口内那 1 份"失败),证明夹具现在真的有分辨力,然后加 `-d` 转绿。
顺带确认:其余三处 `ls -1t`(`agentmail-gateway.bak-*`、`pre-deploy-*.db`、`pre-prune-*.db`)
glob 到的是**文件**,不受影响;插件快照那处(第 294 行)本来就已经写了 `-1dt`。
生产现场实测:`/tmp` 下确实还躺着 1 份 24MB 暂存没被收掉。
## `del()` 的失败分支:采纳 pi 的"让 rm 自己失败"
他指出的第三条路(我原先只想到 immutable 与注入点)是对的:本机以 root 跑、权限拦不住;
`unshare -r` 被拒(`/proc/self/uid_map: Permission denied`);tmpfs 无 `chattr +i`。
**改机器的权限**不如**让 rm 失败**。
实现上走了 `RM="${RM:-rm}"` 而不是 PATH shim,理由:`in_use` 会把命令行里含该路径的进程
判成"在用",而自检必须把路径写在命令行上 —— 实测评 PATH shim 时确实被 `in_use` 挡掉、
`del()` 根本没被调用(那次"测试通过"是假的)。`$RM` 默认就是 `rm`,生产行为逐字不变。
自检新增 4 条(并通过变异确认有区分力:去掉失败判定 ⇒ 强断言变红):
退出码 2、必须打出 `[FAIL] 删不掉`、不许出现收尾汇总、那条路径必须还在。
★ 变异还暴露出一条**弱断言**:单看"退出码 = 2"在变异后**照样通过**(脚本别处也有退 2 的路径)
—— 已在注释里注明它弱、区分力来自另两条,没有让它冒充证据。
★ 顺手修掉一处自指的措辞:我原先在报错里抄了收尾汇总的原话("已删除 N 项"),
于是 `grep -c '已删除'` 命中**这句报错自己** ⇒ "有没有虚报成功"这个检查把自己的措辞
当成了证据。改写成不含该字面量的说法(与 `grep -c 用例名` 是同一族:判据锚在元文本上)。
## pi 的另两点
· `diffSummary` 带 `ctx` 时**会读文件**(判 `scripts.test` 要读两侧原文),不带是纯内存比较
—— 已写进函数头,免得以后有人当纯函数用而在大树上意外吃到 I/O。
· "自检样本不独立"的三种形态(位置选择器 / 共享夹具状态泄漏 / 探针无分辨力)
**合成 docs 第 13 条**(修法同一个:显式命名 + 显式复位),并把上面"夹具形状必须与生产
一致"作为配套一条写进同一条 —— 今天的真 bug 正是它。
验证:prune 自检 22/22、干跑 exit 0;npm test exit 0;drift 自检 35/0;check-shared-libs exit 0。
|
2026-09-14 20:53:54 +08:00 |
|