Files
MailUI4Agents/deploy/check-require-declaration.sh
JianFeeeee a432ae6637 ★ 我自己实测出判据的**第五个洞**: 防空转只判"集合空",**收窄发现域**(find)时集合非空 ⇒ 假绿且自检看不见;补**下界守卫**(调用者 ≥ 3)
★ 洞(pi 没报,是我顺着 pi 那句"_scan_text 内部再调 strip_comments 会共模"往下试出来的):
   ② 防空转判的是 `n_callers -lt 1`(**集合空**)
   ⇒ 把发现域**收窄**时集合**非空**(2 个)⇒ ② **不响**
   ⇒ 而收窄让**域外的违规整批消失** ⇒ 报"裸赋值 0 处"
   实测(树上 install.sh:135 恰是裸赋值 `AGENTMAIL_REQUIRE="x"`):
     把 `find deploy -name '*.sh'` 收窄成 `find deploy -name 'redeploy*.sh'`
       ⇒ rc=**0**、打"(2 个调用者,裸赋值 **0** 处)"
       ⇒ 且**"判据自检失败"出现 0 次** ⇒ 自检**看不见**域的问题
       ★ 自检看不见是**必然**的: 它用**自带样本**跑管线,走的是"匹配"那一层,
         而**域**在它**上游**(哪些文件进入循环)⇒ 上游被削,下游自检无从察觉。
★ 补法: **下界守卫**(`AM_CALLER_MIN=3`,与 criteria-hygiene 的 `gates.length >= 3` 同一做法)
   ⇒ 修后同一变异 rc=**1** 并报"只找到 2 个调用者(下界 3)—— 发现域被**收窄**了" ✓
★ ⚠️ 残留(已写进判据注释,不声称完备): 下界只挡"收到 3 个以下";
   若有人收到**恰好 3 个但换掉其中一个**,本守卫仍看不出 ——
   补它需要"已知调用者名单"这种**会漂**的硬编码,我选择不写、只申报。
★ 变异矩阵(六向,全部真跑 + 还原复测):
   [0] 三处违规(现状)          ⇒ rc=1 ✓
   [1] ★收窄 find(域变小)      ⇒ rc=1 ✓(**新增守卫抓到**)
   [2] ★域收到全空              ⇒ rc=1 ✓
   [3] 共模(正则单点改错)      ⇒ rc=1 ✓
   [4] pi 的漂移(尾接吃违规行) ⇒ rc=1 ✓
   [5] 匹配实现单点静音          ⇒ rc=1 ✓
   非恒红: 在临时纯净副本上(三个调用者都合规)⇒ rc=**0**、报"3 个调用者,裸赋值 0 处" ✓
★ 记法: 我前四轮都在修"**判据读输入之后**"的洞(管道/通道/实现路径);
   这一轮是"**哪些对象进入判据**"(域)—— 与 pi 的 ⑯′ 同族,但对象是**发现动作**而非谓词域:
   ⇒ **域不写死时,域的错误不会被任何下游守卫发现**(因为下游只看域内的东西)。
★ 另: 我没有动 `deploy/install.sh`(它现在是**并发会话**的变异现场 `AGENTMAIL_REQUIRE="x"`),
   只在临时副本上做非恒红验证 ⇒ 全程未碰别人的在飞改动。
2026-09-25 07:31:32 +08:00

225 lines
14 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# 政策:**调用者**声明"我依赖哪些外部命令"**必须**走动作 `agentmail_require`,
# 不许写裸赋值 `AGENTMAIL_REQUIRE="…"`。
#
# # 为什么有这条(一个实测出来的活缺陷)
#
# `deploy/lib/env-defaults.sh` 的 ③b 会检查调用者声明的命令在不在,
# 缺一个就 `exit 2` + 人话("命令不在"与"命令在但输出为空"必须分开 ——
# 后者会让下游把"工具缺失"读成"检查通过",见该文件里 journalctl 那两处假绿)。
#
# ★ 而那段检查**原先只在 `source` 期跑**,三个调用者却都是**先 source 后赋值**:
# install.sh source 131 / 赋值 135
# redeploy-gateway.sh source 42 / 赋值 46
# redeploy-plugin.sh source 52 / 赋值 56
# ⇒ source 那一刻表是**空的** ⇒ `[ -n "${AGENTMAIL_REQUIRE:-}" ]` 直接跳过
# ⇒ **那张表从未被检查过**。实测 A/B:
# AGENTMAIL_REQUIRE=__no_such_cmd . env-defaults.sh ⇒ rc=2(检查本身是好的)
# . env-defaults.sh; AGENTMAIL_REQUIRE=__no_such_cmd ⇒ rc=0(**漏过**)
#
# 修法是两处都挂检查(source 期 + report 内)。但那只堵住"**恰好忘了调 report**"——
# 病根是"**声明**(赋值)与**检查**(调用)是两个可分离的动作"。
# ⇒ 把声明本身做成**动作**:`agentmail_require <命令…>` —— 想声明就**必然**检查,
# "赋了值但那次检查没跑"这个状态**从表示上不存在**。
#
# # 为什么需要**这条判据**(而不是只靠"大家都记得用动作")
#
# 下一个新调用者仍可能写裸赋值 —— 而裸赋值**不报错、不警告**:
# 它只是让那张表在 source 后才存在,于是检查又一次静默失效(与上面那个缺陷同形)。
# ⇒ 判据的作用是**让"写裸赋值"立刻变红**,而不是靠人记住。
#
# # 射程(pi 2026-09-25 §四 要求写清它管什么、不管什么)
#
# **管的集合 = "调用者"**,定义为:`deploy/` 下 `*.sh` 里**真的 source 了**
# `env-defaults.sh` 的那些文件,**减去定义动作的那个库自己**。
# ⇒ 理由是"调用者"这个身份**由 source 这个动作定义**,不由文件名或目录猜。
# ⚠️ 我第一版按"`deploy/**/*.sh` 全扫"划范围,**实测立刻假红**:
# 它把 `env-defaults.sh:449`(`agentmail_require()` 函数体里的
# `AGENTMAIL_REQUIRE="$*"` —— **动作自己的实现**)判成违规。
# ⇒ 那是"动作的定义点",不是"调用者的使用点",两者必须分开。
#
# 管:调用者里**行首**的裸赋值 `AGENTMAIL_REQUIRE=…`(去掉注释后匹配)
# 不管:
# · 定义动作的那个库(`deploy/lib/env-defaults.sh`)—— **它不是自己的调用者**,
# 且它的 `agentmail_require()` 体内**必须**有那行赋值(那是实现,不是违规)
# · `AGENTMAIL_REQUIRE_SELF=…`(**不同名**,与 `=` 匹配天然不撞)
# · 注释里提到 `AGENTMAIL_REQUIRE` 的文字(strip 注释后再匹配 ⇒ 不误伤)
# · **没 source 本库的文件**:它们拿不到 ③b 检查,故"用哪种写法"与本政策无关
# (本政策只保证"**声明了就被检查**",不保证"人人都会声明")
# · **间接**赋值(`v=AGENTMAIL_REQUIRE; eval "$v=…"`、经 `read`/`env` 注入等)
# ⇒ 这类绕过本判据**仍可漏**。本判据盖"字面裸赋值",不声称盖住全部写法。
#
# 退出码:0 = 全走动作式;1 = 有裸赋值 **或**集合为空(防"判据静默空转");2 = 环境不足
set -uo pipefail
REPO="${AGENTMAIL_REPO:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
cd "$REPO" || { echo " [FAIL] 进不去仓库根 $REPO" >&2; exit 2; }
for c in grep awk sed find; do
command -v "$c" >/dev/null 2>&1 || { echo " [FAIL] 环境不足:缺少 $c" >&2; exit 2; }
done
LIB_REL='deploy/lib/env-defaults.sh'
# ★★ 不要写 `strip_comments "$f" | grep -q …`:`set -o pipefail` 下 `grep -q`
# 一命中就退出,`sed` 随即收到 SIGPIPE(rc=141),**管道整体返回 141** ⇒
# `if` 判**假** ⇒ 该文件被漏掉。而它退出的时机取决于缓冲区 ⇒ **结果不稳定**。
# 实测(我第一版):同一条命令两次分别找到 **2** 个与 **1** 个调用者
# (三个调用者只认出一两个),而 `rc=141` 被 `if` 静默吞掉 ——
# 一个**不稳定且偏少**的集合,正好会让"裸赋值 0 处"变成假绿。
# ⇒ 正确做法:先把去注释结果**收集到变量**,再对它做匹配(无管道、无 SIGPIPE)。
#
# ── 去注释的**唯一**实现(全文件只有这一处 `sed`)。
# ★★ pi `597086ad` 实测抓到我"说了但没做到"(这是我的第三次同形):
# 我注释写"正向对照与正式扫描**都走这一个函数**",而旧版 `_scan_text`
# **另写了一份 `sed`**、正式扫描又走 `strip_comments` ⇒ **其实有三份去注释实现**。
# 实测(只把 `strip_comments` 改成"删掉整行"):
# ⇒ 正式扫描静音,而**自检不响**(自检走 `_scan_text` 那份 sed)
# ⇒ 我上一封"自检盖住整条管线 / 已盖住 pi 标的残余"的结论**作废**。
# ⇒ 修法: 去注释收敛到**这一个函数**,匹配收敛到 `_scan_stripped`,
# 正向对照与正式扫描**都经由它们** ⇒ 任一层漂移都同时影响两者 ⇒ 自检必响。
strip_text() { sed 's/#.*$//' <<< "$1"; }
AM_SCAN_RE='^[[:space:]]*AGENTMAIL_REQUIRE='
# 对**已 strip** 的文本做匹配(唯一的匹配实现)
_scan_stripped() { grep -nE "$AM_SCAN_RE" <<< "$1"; }
# 文本 ⇒ 违规行号(= 去注释 + 匹配),供**正向对照**用
_scan_text() { _scan_stripped "$(strip_text "$1")"; }
# ① 找"调用者":非注释行里以 `. ` 或 `source ` 起头、且提到 env-defaults.sh 的文件。
callers=""
for f in $(find deploy -name '*.sh' -type f | sort); do
[ "$f" = "$LIB_REL" ] && continue # 定义点不是调用点(见射程)
body="$(strip_text "$(cat "$f")")"
case "$body" in
*env-defaults.sh*)
# 无管道匹配:`grep` 读文件而非读管道 ⇒ 不会因 SIGPIPE 造出不稳定的 rc。
if grep -qE '^[[:space:]]*(\.|source)[[:space:]].*env-defaults\.sh' <<< "$body"; then
callers="$callers $f"
fi
;;
esac
done
unset body
# shellcheck disable=SC2086
set -- $callers
n_callers=$#
# ② 防空转:集合为空 ⇒ 判**红**,不是"通过"
if [ "$n_callers" -lt 1 ]; then
echo " [FAIL] 一个 source env-defaults.sh 的调用者都没找到 —— 大概率是路径/目录变了。" >&2
echo " 本判据的集合是「调用者」,集合空了它就**静默空转**(恒绿)⇒ 必须判红。" >&2
exit 1
fi
# ②′ ★★ **下界守卫**(2026-09-25 我自己实测出来的洞,不是 pi 报的):
# ② 只判"集合**空**",而**把发现域收窄**(例如把 `find deploy -name '*.sh'`
# 改成 `find deploy -name 'redeploy*.sh'`)时集合**非空**(2 个)⇒ ② 不响。
# 而收窄会让**域外的违规整批消失** ⇒ 判据报"裸赋值 0 处" ⇒ **假绿**。
# 实测: 树上 install.sh:135 是裸赋值,把 find 收窄到 `redeploy*.sh` ⇒
# rc=**0**、打"(2 个调用者,裸赋值 0 处)",且**自检 0 次**(自检看不见域的问题)。
# ⇒ 与 ② 的关系: ② 管"**全丢**"(集合空),本守卫管"**部分丢**"(域被收窄)。
# ⇒ 判法用**下界**: 调用者 ≥ 3(现状: install.sh / redeploy-gateway.sh / redeploy-plugin.sh)。
# 与 `client/electron/test/criteria-hygiene.test.mjs` 里 `gates.length >= 3` 同一做法。
# ⚠️ 残留(如实记): 下界只挡住"收到 3 个以下";若有人收到**恰好 3 个但换掉其中一个**
# (比如只扫 install.sh + 两个不存在的东西),本守卫仍看不出 —— 补它需要
# "已知调用者名单"这种**会漂**的硬编码,我选择不写,改在这里申报。
AM_CALLER_MIN=3
if [ "$n_callers" -lt "$AM_CALLER_MIN" ]; then
echo " [FAIL] 只找到 ${n_callers} 个调用者(下界 ${AM_CALLER_MIN})—— 发现域被**收窄**了。" >&2
echo " 本判据的域是「deploy/ 下所有 source env-defaults.sh 的 *.sh」;" >&2
echo " 域一收窄,域外的违规就整批消失,而集合仍非空 ⇒ **防空转守卫看不见** ⇒ 假绿。" >&2
echo " (实测: 收窄 find 后 rc=0 且打'裸赋值 0 处',而树上确有违规。)" >&2
exit 1
fi
fails=0
for f in "$@"; do
# ★★ 这里**也不能**写成 `strip_comments "$f" | grep -nE …`(我第一版就是):
# 它**目前**不出事只是因为选了 `-n`(`grep -n` 要读完全部输入才退出 ⇒ 没有早退
# ⇒ 生产者不会收到 SIGPIPE)。**换一个字母就翻面** —— pi 2026-09-25 做了可判实验:
# `-n` → `-q` 后,三个调用者的裸赋值**全部被静默漏掉**(rc 由 1 变 **0**)。
# 我复现同一实验:现行 `-n` 报 **3** 个违规 / rc=1;改成 `-q` 报 **0** 个 / rc=**0** ✓
# ⇒ "恰好选了不早退的那个选项"**不是正确性,是运气** —— 而它考的是同一件事:
# 判据读输入的**那一步**能不能把对象静默丢掉(`-n` 与 `-q` 只差"要不要早退")。
# ⇒ 与 :78-82 一致:**先收集到变量,再对变量匹配(无管道)**。
#
# ★★ 第二版(pi 指出我"只修了一半"之后重做)—— 三样一起做,才真的闭掉:
# ① **去管道**(`<<<` here-string)⇒ 生产者不会收 SIGPIPE,`-n`/`-q` 都不再有 141;
# ② **两条独立通道**:"有没有违规"用 `grep -q` 的**退出码**;
# "哪几行"用 `grep -n` 的**输出**。两者各自可靠 ⇒ 输出为空不再能冒充"没有违规"。
# ③ ★ **不一致时 fail-closed**:若"退出码说有"而"行号通道没给出行号"
# (例如有人把取行号那处的 `-n` 写成 `-q` —— `-q` 什么都不打印),
# 则**判据内部自相矛盾** ⇒ **按"有违规"计**,并明确报出这个矛盾。
# 我实测过少了 ③ 会怎样:只做 ①② 时,把取行号那处改成 `-q`
# ⇒ 报 **0** 个违规、rc=**0**(3 个违规**全部静默漏掉**)—— 判定又一次落在输出通道上。
# ⇒ 记法: **"计数"与"证据"若走同一条通道,那条通道静音就等于"没有"**;
# 要要么分通道、要么**规定静音=失败侧**(本处取后者)。
#
# ★★★ 第四版(pi `3be8a091` 指出 ③ **有个我没写出来的前提**,我复现并采纳):
# ③ 的 fail-closed 只在"**两通道不一致**"时触发 ⇒ 它默认两条通道**失效不相关**。
# 而我的两条通道**共享 2 个东西**:(a)**同一个正则字面量**(两处逐字相同)
# (b)**同一个 `$body`**(都来自 `strip_comments "$f"`)。
# ⇒ ★ **单点改动同时哑掉两条时,它们"一致"(都沉默)⇒ `_had=1 ∧ _cnt=0` 不成立
# ⇒ fail-closed 恰好不触发**。pi 的实测我照做复现:
# 把两处正则字面量一起改成 `'^NOMATCH='`(树上**注入着 3 处裸赋值**)
# ⇒ rc=**0**,并打出"裸赋值 **0** 处"—— **与真实情况相反**。
# ⇒ 记法: **"两条通道"要成为独立证据,须"不存在单点改动能同时哑掉两条"** ——
# 条数 ≥ 2 **不是**独立性的判据;要检查它们共享的**字面量/变量/函数/上游命令**。
#
# ★ 补法是**正向对照**(判据自检),且**让它走与正式扫描同一个函数**:
# 用一个**已知违规样本**跑 `_scan_text`,若它匹配不到 ⇒ 说明"沉默"是**共模**的
# ⇒ 按**红**计(不能因为两条通道都不说话就当成"没问题")。
# ★ 为什么走同一函数而不是抄一份正则:抄一份 ⇒ 又出现"两套实现",而
# pi 标的残余正是"正则对、别的环节(body 被过度 strip)静音"——
# 走同一函数能把**整条管线**(strip + 匹配 + 行号输出)一起对照到,
# 而不只对照那个正则字面量。
_pc="$(_scan_text 'x=1
AGENTMAIL_REQUIRE="git go"' || true)"
case "$_pc" in
*AGENTMAIL_REQUIRE*) : ;; # 自检通过:能匹配到已知样本
*)
printf ' [FAIL] 判据自检失败:违规扫描连一个**已知样本**都匹配不到 —— 这是**共模失效**\n' >&2
printf ' (两条通道因同一原因一起沉默 ⇒ fail-closed 的不一致条件不成立)。按红计。\n' >&2
exit 1
;;
esac
unset _pc
# 正式扫描:**先 strip 一次**(唯一实现 `strip_text`),两条通道都用这同一份结果。
_stripped="$(strip_text "$(cat "$f")")"
_had=0
grep -qE "$AM_SCAN_RE" <<< "$_stripped" && _had=1
_cnt=0
while IFS=: read -r _ln _rest; do
[ -z "${_ln:-}" ] && continue
printf ' [FAIL] %s:%s 用了裸赋值(应改为 `agentmail_require <命令…>`)\n' "$f" "$_ln" >&2
_cnt=$((_cnt + 1))
done < <(_scan_stripped "$_stripped")
if [ "$_had" -eq 1 ] && [ "$_cnt" -eq 0 ]; then
printf ' [FAIL] %s 有裸赋值,但**取行号的那条通道没给出任何行号** —— 判据内部不一致;\n' "$f" >&2
printf ' 按 fail-closed 记作 1 处违规(两条通道必须都给得出,否则判定就落在单条通道上)。\n' >&2
_cnt=1
fi
fails=$((fails + _cnt))
unset _stripped _had _cnt _ln _rest
done
if [ "$fails" -gt 0 ]; then
cat >&2 <<'MSG'
为什么必须走动作:`AGENTMAIL_REQUIRE="…"` 只是**赋值**,而检查在
`env-defaults.sh` 的 ③b(source 期)—— 赋值在它**之后** ⇒ 那次检查读到空表
⇒ **整张表从未被检查过**(2026-09-25 实测的活缺陷)。
后续那次检查(report 内)只接住"**记得调 report**"的情形;
写成动作则"赋值与检查"**不可分离** ⇒ 忘不忘都不影响。
改成:
agentmail_require git go npm node curl systemctl flock
MSG
exit 1
fi
awk -v n="$n_callers" 'BEGIN{printf " 通过 调用者声明全走 `agentmail_require` 动作(%d 个调用者,裸赋值 0 处)\n", n}'
exit 0