#!/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)。 strip_comments() { sed 's/#.*$//' "$1"; } # ① 找"调用者":非注释行里以 `. ` 或 `source ` 起头、且提到 env-defaults.sh 的文件。 callers="" for f in $(find deploy -name '*.sh' -type f | sort); do [ "$f" = "$LIB_REL" ] && continue # 定义点不是调用点(见射程) body="$(strip_comments "$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 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 个违规**全部静默漏掉**)—— 判定又一次落在输出通道上。 # ⇒ 记法: **"计数"与"证据"若走同一条通道,那条通道静音就等于"没有"**; # 要要么分通道、要么**规定静音=失败侧**(本处取后者)。 body="$(strip_comments "$f")" _had=0 grep -qE '^[[:space:]]*AGENTMAIL_REQUIRE=' <<< "$body" && _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 < <(grep -nE '^[[:space:]]*AGENTMAIL_REQUIRE=' <<< "$body") 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 body _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