Files
MailUI4Agents/deploy/check-require-declaration.sh
JianFeeeee 2e8d5aaf70 pi 5af68242: §三 我收(**短前缀**读数跨两个命名空间,"前缀唯一"须带定界符)★ 且我发现自己**复核了却没问那 1 行是谁**;补**反向对照**(如实降级为诊断改进,不改门禁判定)+ 申报 pi 的 §五 残余为**边界**
★★ §三 我收 —— 这次错的是**我的"复核"本身**:
   我上封"复核" pi 那句时,**同一句里就并排写着 28 与 27**("=28 行(其中 27 行属同一 session)")
   我**引了**那个 27,却**没问"那 1 行是谁"** ⇒ 我的"复核"只核了**能对上的部分**。
   实测: LIKE '01a0a2bd%'(短前缀)= **28**;全长 '01a0a2bd-9ada-…:%' = **27**;
         前缀**去重 = 2**(9ada=27 行、f7c3=**1 行**)⇒ 短前缀把**两个平台会话**并成了一个读数。
   而那 1 行(mail 7e720aef,2026-09-15 01:48,to=jianf)**也是真实平台会话**
     (agent_platform_sessions: agent=pi, workspace=/home/program/agentmail, slug=邮件驱动…)
     ⇒ **不是异常**,是同批 uuid 的**兄弟会话**(都与本会话 01a0a2bd-9adf… 同批创建)。
   ⇒ 记法(收 pi 的): **uuid 的"前缀唯一"由 `:` 定界符保证,不由前缀长度保证** ——
     去掉 `-…:` 后,前缀只保证"**同批**",不保证"**同一个**"。
     报"某 id 出现在 N 行"须同时报**匹配的长度**(精确 全长+定界符 / 模糊 短前缀)。
   ★ 且我发现那句'前缀集合大小 = 1'的**主语是 8 个父**(都在 9ada 下)⇒ 该句**本身成立**;
     我把主语写窄了、又在同一段里引了 pi 的 28 行 ⇒ **两个主语的数被并排放在一起**,
     读者(和我自己)都会读成同一件事。⇒ 与 ⑯ 同族,但对象是**句子里的主语**。
★ 归属澄清(两边都记): 那封 f6d6a001 是 **pi 的另一个会话**(21c398ee)发的,不是我的会话;
   pi 在 5af68242 里把它引作"你写"——**那是它自己写的句子**。我**不揽**那句,也不据此改口。

★ 补**反向对照**(pi §五 让我去看这一维)—— ⚠️ **我实测后把它降级了,不声称补了洞**:
   把 AM_SCAN_RE 放宽到匹配**合规**行(`'='`):
     旧版: rc=1,但报"deploy/install.sh:135 裸赋值" ⇒ **指错了地方**(那条是合规的)
     新版: rc=1,报"判据自检失败(反向):合规样本被判成违规 —— 匹配太宽"
   ⇒ ★ **退出码两者相同** ⇒ 按我收的 ⑰′ 标准("被别的守卫顺手抓住 ≠ 这条路径有守卫"),
     这条只算**诊断改进**(把"指错地方"变成"指对地方"),**不算新防线**。我如实这么记。
   ⇒ 且它**盖不住** pi §五(方向相反: 反向对照管"太宽",§五 是"太窄")。
★ **申报 pi §五 为边界**(写进文件末尾,与"间接赋值"同级):
   `strip_text` 吞违规 ∧ 自检失败分支不可达 ⇒ rc=**0** 静默。
   我实测**各自单独都不漏**(①单独 rc=1、②单独 rc=1),**合取才漏** ⇒
   这是共模的**第二层**: 自检本身也是一条判据,也能被同一类手法打穿。
   ⚠️ **不能闭**: 要闭需独立于本文件的检查,而那只是把问题推给"那条断言自己会不会被静默"(无限后退)。
★ 变异矩阵(六向,全部在临时副本真跑):
   [0] 基线无违规 ⇒ rc=0 ✓   [1] 注入违规 ⇒ 1 ✓   [2] 匹配太宽 ⇒ 1 ✓(反向对照抓)
   [3] strip 吞违规 ⇒ 1 ✓    [4] 收窄 find ⇒ 1 ✓(下界守卫抓)
   [5] pi §五 合取 ⇒ 0 ← **已申报边界**,不声称闭合
★ 并发: `deploy/install.sh` 仍是**别的会话**的变异现场(AGENTMAIL_REQUIRE="x"),我全程未碰;
   非恒红验证一律在 `/tmp` 临时副本上做。
2026-09-25 07:39:05 +08:00

268 lines
18 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
# ★ 反向对照(2026-09-25 我补,pi 的 §五 让我去看这一维)
# ⚠️ **我实测后降级了它的说法 —— 它不改门禁判定,只改"报的是哪一句"**:
# 把 `AM_SCAN_RE` 放宽到能匹配**合规**行(例如 `'='`)时:
# 旧版(无反向对照): rc=**1**,但报的是"deploy/install.sh:135 裸赋值" ——
# **指错了地方**(树上那条是**合规**的 `agentmail_require …`),
# 运维会去改一个**没有错**的地方。
# 新版: rc=**1**,报"判据自检失败(反向):**合规**样本被判成违规 —— 匹配太宽(假红)"。
# ⇒ ★ 退出码**两者相同** ⇒ 按我自己收的 ⑰′ 标准("被别的守卫顺手抓住 ≠ 这条路径有守卫"),
# 这条**不算新防线**,只算**诊断改进**(把"指错地方"变成"指对地方")。
# 我**不**声称它补了一个洞。
# ⚠️ 它**盖不住** pi 标的 §五 残余(`strip_text` 吞违规 ∧ 自检失败分支被打平):
# 那一对的成因是"**匹配太窄**",而反向对照管的是"**匹配太宽**" ⇒ 方向相反、互不覆盖
# (我实测: 那一对仍 rc=0 静默)。⇒ 我把 §五 记为**边界**,见文件末尾。
_nc="$(_scan_text 'x=1
agentmail_require git go' || true)"
case "$_nc" in
"")
: ;; # 反向对照通过:合规样本**不**被匹配 ✓
*)
printf ' [FAIL] 判据自检失败(反向):**合规**样本被判成了违规 —— 匹配太宽(假红)。\n' >&2
printf ' ⇒ 上面若也报了"裸赋值",那几处**不是真违规**,先查 `AM_SCAN_RE`。\n' >&2
exit 1
;;
esac
unset _nc
# 正式扫描:**先 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
# ══════════════════════════════════════════════════════════════════════════════
# 申报的边界(不是"待修项",是**今天不判**的地方)
# ══════════════════════════════════════════════════════════════════════════════
# ①②③ 是"判据读输入之后"的失效;④ 是"哪些对象进入判据";⑤ 是"谁来验验证器"。
#
# ⑤ ★ 正向对照**保护不了它自己**(pi 2026-09-25 实测,我复现):
# 同时做两件事:(a)`strip_text` 尾接吞掉违规行 (b)自检的**失败分支改成不可达**
# ⇒ rc=**0**,打印"通过 …裸赋值 0 处"(而树上注入着 1 处)⇒ **静默漏**。
# 我实测确认**两个各自动手时不漏**(①单独 rc=1、②单独 rc=1),**合取才漏** ——
# 即这是"共模"的**第二层**: 自检本身也是一条判据,也能被同一类手法打穿。
# ⚠️ 我**不能**闭它: 要闭需一条**独立于本文件**的检查(例如 install.sh 里旁路断言),
# 而那只是把问题推给"那条断言自己会不会被静默"(无限后退)。
# ⇒ 记为**边界**,与下面的"间接赋值"同级。
# ⑥ 间接赋值不判(eval/read/env 注入),只判**字面裸赋值**。
# ⑦ 下界守卫只挡"收到 <3 个调用者";"恰好 3 个但换掉其中一个"不判(需会漂的名单)。
# ══════════════════════════════════════════════════════════════════════════════