采纳 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 型判据"的射程。
This commit is contained in:
127
deploy/check-require-declaration.sh
Executable file
127
deploy/check-require-declaration.sh
Executable file
@ -0,0 +1,127 @@
|
||||
#!/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
|
||||
hits="$(strip_comments "$f" | grep -nE '^[[:space:]]*AGENTMAIL_REQUIRE=' || true)"
|
||||
[ -z "$hits" ] && continue
|
||||
while IFS= read -r h; do
|
||||
[ -z "$h" ] && continue
|
||||
printf ' [FAIL] %s:%s 用了裸赋值(应改为 `agentmail_require <命令…>`)\n' "$f" "${h%%:*}" >&2
|
||||
fails=$((fails + 1))
|
||||
done <<< "$hits"
|
||||
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
|
||||
@ -132,7 +132,7 @@ fi
|
||||
# 本脚本依赖的外部命令:缺一个就 exit 2 + 人话(见 env-defaults.sh 的 ③b 一节)。
|
||||
# 为什么必须显式声明:**"命令不在"与"命令在但输出为空"必须分开** ——
|
||||
# 下游把前者读成后者时就会产出假绿(journalctl 那两处就是:工具缺失被读成"无 panic")。
|
||||
AGENTMAIL_REQUIRE="git go npm npx node curl systemctl flock"
|
||||
agentmail_require git go npm npx node curl systemctl flock
|
||||
agentmail_env_report
|
||||
|
||||
# 与 `redeploy-gateway.sh` / `reset-demo.sh` 同源(pi 评审 2026-09-14 对出来的):
|
||||
@ -375,6 +375,22 @@ else
|
||||
"$REPO/deploy/check-file-modes.sh"
|
||||
fi
|
||||
|
||||
# 调用者声明政策:`deploy/` 下的调用者必须用 `agentmail_require` 动作声明依赖的命令,
|
||||
# 不许写裸赋值 `AGENTMAIL_REQUIRE="…"`(裸赋值与那次检查**可分离** ⇒ 检查会静默失效)。
|
||||
# 理由与射程见 `deploy/check-require-declaration.sh` 头注释。
|
||||
# ★ 与上面 `check-file-modes.sh` **同样走 `CHECK_GATE_RC` 累积通道**(理由同上,不再重复):
|
||||
# 直接调会在 `set -e` 下中止,把后面的收尾诊断全部吃掉。
|
||||
if [[ $CHECK_ONLY -eq 1 ]]; then
|
||||
_rd_rc=0
|
||||
"$REPO/deploy/check-require-declaration.sh" || _rd_rc=$?
|
||||
if [[ $_rd_rc -ne 0 ]]; then
|
||||
echo " [FAIL] 调用者声明政策没过(退出码 ${_rd_rc})—— 上面标了 [FAIL] 的那几处就是" >&2
|
||||
[[ -n "${CHECK_GATE_RC:-}" ]] || CHECK_GATE_RC="$_rd_rc"
|
||||
fi
|
||||
else
|
||||
"$REPO/deploy/check-require-declaration.sh"
|
||||
fi
|
||||
|
||||
# 插件的纯函数测试(自动转发去重等)。
|
||||
# 插件不参与构建产物,但它的逆行为会直接变成用户收件箱里的重复邮件,
|
||||
# 因此也纳入部署前的门禁。zod 已在 node_modules 里就不重装。
|
||||
|
||||
@ -420,9 +420,13 @@ unset _am_avail_kb
|
||||
# (三个调用者都在赋值后**立刻**调它)。两处都留,因为两条路都要接住:
|
||||
# · source 期那次 —— 接"表由**环境**提供"(父进程 export / CI 注入)
|
||||
# · report 期那次 —— 接"表由**调用者**赋值"(本仓三个调用者都是这一种)
|
||||
# ⚠️ 残留缺口(如实申报,不假装已闭合):若某个调用者**既不 export 表、又忘了调
|
||||
# `agentmail_env_report`**,则两次都不跑 ⇒ 仍会漏。闭合它需要"report 没被调也要红",
|
||||
# 而那要求本文件知道调用者的控制流 —— 做不到。⇒ 记为已知边界,不写成"已修好一切"。
|
||||
# ⚠️ 残留缺口(**已由 pi 的反提案闭合,见下** —— 原文如实保留一段):
|
||||
# 若某个调用者**既不 export 表、又忘了调 `agentmail_env_report`**,则两次都不跑 ⇒ 仍会漏。
|
||||
# 我原判"闭合它需要本文件知道调用者的控制流(report 没被调也要红)—— 做不到"。
|
||||
# ★★ pi 2026-09-25 指出**前提可以换掉**(我复核采纳):病根不是"控制流不可知",
|
||||
# 而是"**声明**(赋值)与**检查**(调用)是两个可分离的动作"。
|
||||
# ⇒ 把声明本身做成**动作**:`agentmail_require <命令…>` —— 想声明就**必然**检查,
|
||||
# "声明了但没检查"这个状态**从表示上不存在**。
|
||||
agentmail_env_check_require() {
|
||||
[ -n "${AGENTMAIL_REQUIRE:-}" ] || return 0
|
||||
local _am_missing="" _am_c
|
||||
@ -436,12 +440,22 @@ agentmail_env_check_require() {
|
||||
exit 2
|
||||
fi
|
||||
}
|
||||
|
||||
# ★★ 调用者**用这个**声明自己依赖的命令(而不是裸 `AGENTMAIL_REQUIRE=…`)。
|
||||
# 声明与检查**同一个动作** ⇒ "赋了值但那次检查没跑"不再是可表示的状态。
|
||||
# (旧写法 `AGENTMAIL_REQUIRE="…"` + 稍后调 report 仍然有效 —— report 里还有一次检查,
|
||||
# 留它接"表由**环境**提供"(父进程 export / CI 注入)那条路。)
|
||||
agentmail_require() {
|
||||
AGENTMAIL_REQUIRE="$*"
|
||||
agentmail_env_check_require
|
||||
}
|
||||
agentmail_env_check_require
|
||||
|
||||
# 调用者打一行,让"兜了什么"**可见**(也是"忘了 source"的信号 —— 少了这行就不对)。
|
||||
agentmail_env_report() {
|
||||
# ★ 在这里**再跑一次** ③b(见上 ★★ 相位缺陷):调用者的 `AGENTMAIL_REQUIRE=` 是
|
||||
# source **之后**才赋的,source 期那次看不到它。三个调用者都在赋值后立刻调本函数。
|
||||
# ★ 在这里**再跑一次** ③b(见上 ★★ 相位缺陷):调用者的 `AGENTMAIL_REQUIRE` 若由
|
||||
# **环境**(父进程 export / CI 注入)提供,source 期那次就能看到;若由**调用者赋值**,
|
||||
# 则靠 `agentmail_require` 动作或本行这次。三条路都接住。
|
||||
agentmail_env_check_require
|
||||
if [ -n "$AGENTMAIL_ENV_DEFAULTS" ]; then
|
||||
printf ' 环境兜底:%s\n' "$AGENTMAIL_ENV_DEFAULTS"
|
||||
|
||||
@ -43,7 +43,7 @@ PREFIX="${AGENTMAIL_PREFIX:-/opt/agentmail}"
|
||||
# 本脚本依赖的外部命令:缺一个就 exit 2 + 人话(见 env-defaults.sh 的 ③b 一节)。
|
||||
# 为什么必须显式声明:**"命令不在"与"命令在但输出为空"必须分开** ——
|
||||
# 下游把前者读成后者时就会产出假绿(journalctl 那两处就是:工具缺失被读成"无 panic")。
|
||||
AGENTMAIL_REQUIRE="git go npm node curl systemctl journalctl sqlite3 install flock"
|
||||
agentmail_require git go npm node curl systemctl journalctl sqlite3 install flock
|
||||
agentmail_env_report
|
||||
|
||||
# ★ root 断言(pi 评审 2026-09-14 指出本脚本缺它,只有 install.sh 有):
|
||||
|
||||
@ -53,7 +53,7 @@ REPO=${REPO:-/home/program/agentmail}
|
||||
# 本脚本依赖的外部命令:缺一个就 exit 2 + 人话(见 env-defaults.sh 的 ③b 一节)。
|
||||
# 为什么必须显式声明:**"命令不在"与"命令在但输出为空"必须分开** ——
|
||||
# 下游把前者读成后者时就会产出假绿(journalctl 那两处就是:工具缺失被读成"无 panic")。
|
||||
AGENTMAIL_REQUIRE="git node npx systemctl journalctl sqlite3 flock"
|
||||
agentmail_require git node npx systemctl journalctl sqlite3 flock
|
||||
agentmail_env_report
|
||||
|
||||
# ★ **部署锁**(pi 评审 2026-09-14):环境前提里原先缺的第五列 —— **同时性**。
|
||||
|
||||
Reference in New Issue
Block a user