修 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(越权)或另设入口。
This commit is contained in:
2026-09-25 04:35:21 +08:00
parent 753310d12f
commit de1b07210e
2 changed files with 161 additions and 41 deletions

View File

@ -43,6 +43,67 @@ if [[ $CHECK_ONLY -eq 1 ]]; then
export AGENTMAIL_ENV_DEFER
fi
# ── 版本库钩子:把 `core.hooksPath` 指向仓库里的 `.githooks/` ──
#
# ⚠️⚠️ **这一块必须在 `source env-defaults.sh` 之前**(pi 2026-09-25 的破坏性测试逼出来的):
# 它只用 `git` 与 `echo`,**不碰 Go/Node**,却曾经被 GOCACHE 预检挡在门外 ⇒
# `--git-hooks` 在新机器上 exit 2 ⇒ **到不了这里** ⇒ 钩子永远装不上。
# 而"新机器"**正是**钩子落盘唯一要解决的场景(本仓库当前的 hooksPath 只是本地配置,
# 不随仓库走 —— 新 clone 里 hook 文件在,`git` 却不会调用它)。
# ⇒ 旧位置(在 source 之后)等于:**能装的机器不需要装,需要装的机器装不上**。
#
# 为什么要它、而不只靠判据(pi 2026-09-15):
# `criteria-hygiene` 里那条 AGC 判据读的是 **index**(`git ls-files`)——
# 它守的是"不会再被加回来",**不是**"不会被推出去"。而 blob 此刻正躺在
# **未推送的提交**里:`git rm --cached` 之后它从 index 消失了,历史里却还在,
# 下一次 `git push` 会连它一起发出去,**而没有任何东西会红**。
# ⇒ 判据是**泄露之后**响的闹钟;这个钩子是**不让它泄露**。两件都要有。
#
# 钩子**跟着仓库走**(`.githooks/` 进版本库),所以换台机器 clone 下来装一次即可;
# 这也是它比"写进 `.git/hooks/`"强的地方 —— 后者不进版本库、别人 clone 不到。
#
# ★ 判据要问的是"**git 会不会调用它**",不是"文件在不在"(pi 2026-09-25 §三):
# 用 `git rev-parse --git-path hooks/<name>` 把**配置解析成真实路径**再查可执行位。
# 只查 `.githooks/pre-commit` 存在,会在 hooksPath 为空时**照样判绿** ——
# 那正是我们反复记的"存在 ≠ 生效"。
git_hook_active() {
# $1 = 钩子文件名。输出:可执行的真实路径(成功)或空(失败)。
# --path-format=absolute:不加它 rev-parse 给的是**相对**路径,直接 test -x 会按 cwd 解析 ⇒ 误判。
local p
p="$(git -C "$REPO" rev-parse --path-format=absolute --git-path "hooks/$1" 2>/dev/null)" || return 1
[[ -n "$p" && -x "$p" ]] || return 1
printf '%s' "$p"
}
if [[ $GIT_HOOKS -eq 1 ]]; then
# ⚠️ 本块在 `source env-defaults.sh` 之前 ⇒ 拿不到它那条 `AGENTMAIL_REQUIRE` 检查,
# 所以这里**自己**判一次 `git` 在不在。否则缺 git 时得到的是 bash 的
# `git: command not found`(退出码 127)—— 而本仓约定"环境问题=2、且要给人话"。
# (这是我把本块前移时实测撞出来的:127 不在本仓的退出码词汇表里。)
if ! command -v git >/dev/null 2>&1; then
echo " [FAIL] 环境不足:找不到 git(--git-hooks 只用 git,但它不在 PATH 里)" >&2
echo " 药方:装 git,或把它的目录加进 PATH 后重跑。" >&2
exit 2
fi
git -C "$REPO" config core.hooksPath .githooks
echo "==> 已接 git 钩子:core.hooksPath=.githooks"
# 自证:接上之后**真的要能拦**。只设配置不验证 = "门是好的 ≠ 门接着"。
for h in pre-push pre-commit; do
if p="$(git_hook_active "$h")"; then
echo " [ OK ] git 会调用 $h($p)"
else
# pre-push 是硬门(推送泄露);pre-commit 只是围栏 gate,旧 clone 可能还没拉到 ⇒ 不 fail
if [[ "$h" == "pre-push" ]]; then
echo " [FAIL] git 不会调用 $h:配置指过去了也没有东西跑(核 core.hooksPath 与 .githooks/$h 的可执行位)" >&2
exit 1
fi
echo " [WARN] git 不会调用 $h —— 围栏 gate 未生效(提交不会被拦)" >&2
fi
done
if [[ $CHECK_ONLY -eq 0 ]]; then
exit 0 # --git-hooks 是独立动作,不连带装服务
fi
fi
# 环境自足:一处给全(HOME/TMPDIR/GOMODCACHE/PATH),不在每条命令旁各写预检。
# 理由与四次历史见该文件的头注释 —— pi 的原话是"否则第五次一定会来"。
# shellcheck source=./lib/env-defaults.sh
@ -61,40 +122,6 @@ PREFIX="${AGENTMAIL_PREFIX:-/opt/agentmail}"
ETC=/etc/agentmail
# ── 版本库钩子:把 `core.hooksPath` 指向仓库里的 `.githooks/` ──
#
# 为什么要它、而不只靠判据(pi 2026-09-15):
# `criteria-hygiene` 里那条 AGC 判据读的是 **index**(`git ls-files`)——
# 它守的是"不会再被加回来",**不是**"不会被推出去"。而 blob 此刻正躺在
# **未推送的提交**里:`git rm --cached` 之后它从 index 消失了,历史里却还在,
# 下一次 `git push` 会连它一起发出去,**而没有任何东西会红**。
# ⇒ 判据是**泄露之后**响的闹钟;这个钩子是**不让它泄露**。两件都要有。
#
# 钩子**跟着仓库走**(`.githooks/` 进版本库),所以换台机器 clone 下来装一次即可;
# 这也是它比"写进 `.git/hooks/`"强的地方 —— 后者不进版本库、别人 clone 不到。
if [[ $GIT_HOOKS -eq 1 ]]; then
git -C "$REPO" config core.hooksPath .githooks
echo "==> 已接 git 钩子:core.hooksPath=.githooks"
# 自证:接上之后**真的要能拦**。只设配置不验证 = "门是好的 ≠ 门接着"。
if [[ -x "$REPO/.githooks/pre-push" ]]; then
echo " [ OK ] .githooks/pre-push 存在且可执行"
else
echo " [FAIL] .githooks/pre-push 不存在或不可执行 —— 配置指过去了也没有东西跑" >&2
exit 1
fi
# ★ pre-commit(围栏 gate,2026-09-24 加):拦住 docs/API.md 围栏奇数/未配对。
# 与 pre-push 同一理:内联 gate 只活在"那次上下文",落盘成 hook 才能被未来的自己/别人复用。
if [[ -x "$REPO/.githooks/pre-commit" ]]; then
echo " [ OK ] .githooks/pre-commit 存在且可执行"
else
echo " [WARN] .githooks/pre-commit 不存在或不可执行 —— 围栏 gate 未落盘(提交不会被拦)" >&2
# 不 fail:旧 clone 还没拉到这个 hook,不该挡住 --git-hooks 本身
fi
if [[ $CHECK_ONLY -eq 0 ]]; then
exit 0 # --git-hooks 是独立动作,不连带装服务
fi
fi
# ★ **部署锁**(pi 评审 2026-09-14):环境前提里原先缺的第五列 —— **同时性**。
# 这台机器的工作区是多 agent 共用的(docs/DEV-TOOLING.md 自己记过),并发部署会互相踩:
# 两次写同一个目录/文件、两次后置验证互相把对方的"验证不过"当自己的结论、谁回滚不确定。
@ -416,20 +443,24 @@ if [[ $CHECK_ONLY -eq 1 ]]; then
hooks_path="$(git -C "$REPO" config --get core.hooksPath || true)"
if [[ "$hooks_path" == ".githooks" ]]; then
echo " [ OK ] git 钩子已接(core.hooksPath=.githooks)"
# 逐钩自证:接上 ≠ 存在 ≠ 可执行
if [[ -x "$REPO/.githooks/pre-push" ]]; then
echo " [ OK ] .githooks/pre-push 存在且可执行(拦 AGC 真身进远端)"
# 逐钩自证:接上 ≠ 存在 ≠ 可执行。
# ★ 用 `git_hook_active` 而不是写死 `$REPO/.githooks/<h>`(pi 2026-09-25 §三 逼出来的):
# 写死路径只证明"**那个文件**在",不证明"**git 会调用它**"——
# 配置指向别处、或 hook 少了可执行位时,前者照样绿。这正是"存在 ≠ 生效"。
if p="$(git_hook_active pre-push)"; then
echo " [ OK ] git 会调用 pre-push($p)—— 拦 AGC 真身进远端"
else
echo " [WARN] .githooks/pre-push 不存在/不可执行 —— pre-push 形同虚设" >&2
echo " [WARN] git 不会调用 pre-push —— pre-push 形同虚设(核 core.hooksPath 与可执行位)" >&2
fi
if [[ -x "$REPO/.githooks/pre-commit" ]]; then
echo " [ OK ] .githooks/pre-commit 存在且可执行(拦 docs/API.md 围栏奇数/未配对)"
if p="$(git_hook_active pre-commit)"; then
echo " [ OK ] git 会调用 pre-commit($p)—— 拦 docs/API.md 围栏奇数/未配对"
else
echo " [WARN] .githooks/pre-commit 不存在/不可执行 —— 围栏 gate 未落盘(提交不会被拦)" >&2
echo " [WARN] git 不会调用 pre-commit —— 围栏 gate 未落盘(提交不会被拦)" >&2
fi
else
echo " [WARN] git 钩子**没接**:core.hooksPath=${hooks_path:-(未设 → 用 .git/hooks,里面只有 sample)}"
echo " 后果:推送前**没有任何东西**拦 AGC 真身进远端历史(判据只盖 index,盖不住 push)。"
echo " ★ 以及:提交前也**不会**拦 docs/API.md 的围栏 gate —— 文件在仓库里,但 git 不调用它。"
echo " 接上:./deploy/install.sh --git-hooks(只写本地 git 配置,不需要 root)"
fi