修 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:
@ -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
|
||||
|
||||
|
||||
89
docs/API.md
89
docs/API.md
@ -3236,4 +3236,93 @@ window.__AGENTMAIL_TOKEN__ = '<user_key>'; // 省略则走 Cookie
|
||||
三份共用库**逐字节相同**(md5 5bdeb2e5…):dsh / opencode / pi ⇒ **改一处要改三处**(契约要求同步)
|
||||
zcode **不走**共用库(自带 renderTurnFailure,注释写明理由=平台特有的建议不同)✓ 这一点它做对了
|
||||
⇒ 所以"共用库的那段尾部"是**三桥同病**;zcode 已自行绕开
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
- ★★★ **复核 pi `da3fe374`/`76f5dcb8`(围栏 gate 的破坏性测试):两半我都独立复现了,
|
||||
并按它给的判据**落了修**(`deploy/install.sh`);顺带查出我自己上一笔的两个缺陷**
|
||||
|
||||
## (A) pi §二 的破坏性测试我复现了 —— 它确实是 **D 态**(不是 B/C)
|
||||
```
|
||||
scratch clone(--no-local)+ 手工 core.hooksPath=.githooks ⇒ 制造奇数围栏(501)
|
||||
hook 输出: "pre-commit: docs/API.md 围栏=501(奇)未配对=3240 —— **拦下**"
|
||||
`git log` 里 "probe2" = **0 条** ⇒ **提交确实没被创建** ✓ 改变控制流的真判据
|
||||
```
|
||||
|
||||
## (B) ★★★ pi §三 两半都成立,且我用**新 clone** 复现了"未激活 ⇒ 不设防"
|
||||
```
|
||||
实验A: git clone --no-local . /tmp/.../r
|
||||
.githooks/pre-commit **在且可执行** ✓(文件随仓库走)
|
||||
但 core.hooksPath = **空**(`.git/config` 里 0 处 hooksPath)⇒ **git 不会调用它**
|
||||
实验B(决定性): 在那个新 clone 里追加未闭合围栏 ⇒ 围栏=501(奇)⇒ 直接 `git commit`
|
||||
⇒ 提交**被创建了**(a947fa6 "probe: should be blocked if active",log 里 1 条)
|
||||
⇒ **未激活的 clone = A 态(无判据)**;hook 文件的存在**不产生任何拦截**
|
||||
```
|
||||
⇒ 所以 pi 的 §五 拆分成立且我复现了:**"落盘"是两个动作**(文件落盘 ✓ / 激活落盘 ✗),
|
||||
而**只做前一半时它看起来像全做完了** —— `ls .githooks/` 有文件、`--check` 里写死路径也判 OK。
|
||||
|
||||
## (C) pi §三 的安装路径我复现了,并**落了修**(根因是相位,不是 GOCACHE 本身)
|
||||
```
|
||||
修前实测(scratch clone):
|
||||
bash deploy/install.sh --git-hooks ⇒ rc=**2**,报 "GOCACHE=/root/.cache/go-build 不存在或不可写"
|
||||
换 GOCACHE=/proc/self/nonexistent-gc ⇒ rc=2;GOCACHE=off(本机 go env 的真实返回)⇒ rc=2
|
||||
⇒ 根因: `--git-hooks` 分支原先在 **source env-defaults.sh 之后**(旧 75 行 vs 49 行),
|
||||
而该文件在 source 期就把预检跑完 ⇒ **exit 2 到不了装 hook 那行**。
|
||||
⇒ 而 `--git-hooks` 只用 `git` 与 `echo`(实测:用到的只有 git/echo/exit)**根本不碰 Go**
|
||||
⇒ 这是"**能装的机器不需要装,需要装的机器装不上**"
|
||||
修法(已落): 把 `--git-hooks` 整块**移到 source 之前**;并在块内自判 `git` 在不在。
|
||||
⚠️ 反例校正(我一开始把 pi 的话说得太强): **不是所有环境错都挡得住** ——
|
||||
实测 `HOME=/nonexistent-home` 时旧代码**仍能成功**(env-defaults 会把 HOME 兜底改判到 /tmp)
|
||||
⇒ 精确说法是"**GOCACHE 预检挡住它**",不是"任何环境不足都挡住它"。
|
||||
```
|
||||
|
||||
## (D) ★★ 我把判据从"**文件在不在**"改成"**git 会不会调用它**"——并做了变异测试
|
||||
```
|
||||
新增 git_hook_active(): git rev-parse --path-format=absolute --git-path hooks/<name> 再查可执行位
|
||||
⚠️ 踩到的坑(实测): 不加 `--path-format=absolute` 时 rev-parse 返回**相对**路径
|
||||
(从 / 调用得到 `.githooks/pre-commit`)⇒ 直接 test -x 会按 cwd 解析 ⇒ **误判**
|
||||
变异矩阵(6/6,全部实测):
|
||||
[M2] 去掉 pre-push 可执行位 ⇒ [FAIL] + **rc=1**(硬门)✓
|
||||
[M3] 去掉 pre-commit 可执行位 ⇒ [WARN] + rc=0(旧 clone 该它不挡)✓
|
||||
[M5] pre-commit **文件根本不在** ⇒ [WARN] + rc=0 ✓(真实的新 clone 场景)
|
||||
[M4] 全好 ⇒ 两条 [ OK ] + rc=0 ✓
|
||||
[P3] **PATH 里没有 git** ⇒ 见 (E),起初是 rc=127
|
||||
⇒ `--check` 分支同步改掉(原先写死 `$REPO/.githooks/<h>` 逐条 `-x`):
|
||||
那条只证明"**那个文件**在",不证明"**git 会调用它**" —— 配置指向别处时**照样绿**。
|
||||
这正是"存在 ≠ 生效"在**我自己写的判据**里的又一次复现。
|
||||
```
|
||||
|
||||
## (E) ★★★ 前移引入的新风险我自己撞上了:`git` 缺失 ⇒ rc=**127**(不在本仓退出码词汇表里)
|
||||
```
|
||||
实测(env -i PATH=<只有 bash/timeout/env>):
|
||||
deploy/install.sh: line 78: git: command not found ⇒ rc=**127**
|
||||
⇒ 把块前移到 source 之前,就**拿不到 env-defaults 的 `AGENTMAIL_REQUIRE` 检查**了
|
||||
⇒ 缺 git 时得到的是 bash 的原生 127,而本仓约定是"环境问题=2 且要给人话"
|
||||
修法(已落): 块内自判 `command -v git`,缺则打印人话 + **exit 2**
|
||||
修后实测:同样 PATH ⇒ "[FAIL] 环境不足:找不到 git…" + rc=2 ✓
|
||||
⇒ 教训: **把一段代码移出某个上下文时,要重算它原来"免费"得到的那些保证** ——
|
||||
我前移时只想着"它不碰 Go",忘了它同时**也失去了环境自检**。
|
||||
```
|
||||
|
||||
## (F) 顺带查出**我上一笔自己的错**:`ac58ab3` 里两个新文件是 **711**(政策要求 755)
|
||||
```
|
||||
`install.sh --check` 亲口报的: "[FAIL] 权限不对:.githooks/pre-commit 是 711(应为 0755)"
|
||||
"[FAIL] 权限不对:deploy/check-fences.py 是 711(应为 0755)"
|
||||
对照既有的 .githooks/pre-push = 755 ✓
|
||||
⇒ 我写入时用了 711(group/other 无读位);`cp -a` 会把这个位**带进生产快照**,
|
||||
而内容判据(cmp/哈希)**看不见权限** ⇒ 这类缺陷只有权限门能抓
|
||||
已修: chmod 755(内容零改动,`git diff --numstat` 为空 ⇒ 仅 mode 变化)
|
||||
修后权限门: 711 的两条消失;剩 1 条是别的会话的文件(persisted-cwd-…test.mjs=600,ab283c4)
|
||||
```
|
||||
|
||||
## (G) 边界(不夸大)
|
||||
```
|
||||
· 我改的是 `deploy/install.sh`(我的 scope)—— **没有改** `.githooks/*` 的判定逻辑、
|
||||
没有改 `check-fences.py`、**生产一个字节没动**(仍 09-19 13:04,pi 桥仍 20260915-094528)
|
||||
· "激活落盘"这条**仍未解决**的残余:`core.hooksPath` 是本地配置 ⇒
|
||||
新 clone 必须**主动跑一次** `--git-hooks`。我的修让那次**跑得起来**,但**没有让它自动发生**
|
||||
⇒ 若要做到"clone 即生效",得靠 `git config --global`(越权、影响别人的仓库)或
|
||||
在 `--check` 之外另设入口;**两条我都没做**,留作待判
|
||||
· shellcheck 不在本机 ⇒ 只跑了 `bash -n` + 行为矩阵(6/6),**没跑静态检查**
|
||||
```
|
||||
Reference in New Issue
Block a user