★ 背景: 我上封把"调用者既不 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 型判据"的射程。
468 lines
30 KiB
Bash
468 lines
30 KiB
Bash
#!/usr/bin/env bash
|
||
#
|
||
# 部署脚本的**环境自足**声明:一处给全,不再每条命令旁各写一个预检。
|
||
#
|
||
# # 为什么要有这个文件
|
||
#
|
||
# 2026-09-14 这条链上一共修了**四次**同一形状的错,每次形态一模一样:
|
||
# **部署路径上假设了一个环境变量存在**。四次都是"再加一个预检",
|
||
# 而预检只挡**已知的那一个** —— 挡住 `HOME` 之后 `GOMODCACHE` 才露出来
|
||
# (两者其实是同一个根因的两面:`HOME` 决定 go 默认缓存位置)。
|
||
# pi 的原话:**"否则第五次一定会来。"**
|
||
#
|
||
# 四次记录(每次都是实测,不是推测):
|
||
# 1. `HOME` 未设 ⇒ `go` 拒绝工作(`homeagent` 的 build.sh,当次修了一处)
|
||
# 2. `HOME` 未设 ⇒ 同一族再现(`install.sh`,当次又修一处)
|
||
# 3. `TMPDIR` 未设/不可写 ⇒ 构建与测试把临时文件写到意外位置
|
||
# (`redeploy-plugin.sh:160` 曾写成 `TMPDIR=${TMPDIR:-/tmp} npx tsc …`,即就地兜了一次)
|
||
# 4. `GOMODCACHE`/`GOPATH` 都未设(`HOME` 也没设)⇒
|
||
# `go: module cache not found: neither GOMODCACHE nor GOPATH is set`
|
||
#
|
||
# # 纪律
|
||
#
|
||
# · 需要环境的脚本**开头** `source` 本文件,别在命令行里内联兜底;
|
||
# · 本文件只设**未设**的变量(`:-` 语义),绝不覆盖调用者已经给的值;
|
||
# · 失败要**说清是环境问题**并按仓库约定用退出码 2(环境),不要让它冒充代码缺陷。
|
||
#
|
||
# ⚠️ **顺序不是风格,是正确性**(pi 评审 2026-09-14):
|
||
# 下面 ① → ④ 的顺序有依赖 —— ③ 用 `command -v go` 探测,而 `command -v` **走 PATH**,
|
||
# 所以 ④(修 PATH)必须在 ③ **之前**。原先反了:在 ④ 要修的那个环境里(PATH 为空)、
|
||
# ③ 的探测必然失败,紧接着 ④ 把 PATH 装上,后面的步骤**又能**找到 go
|
||
# —— **探测结论与实际可用性相反**。一条探测所依赖的东西,正是同一文件后面那一步要修的
|
||
# 东西:这正是本文件存在的理由(消除"部署路径上的环境假设"),所以顺序写死在这里并解释。
|
||
#
|
||
# ⚠️ 本文件被 `source`,**不能带 `set -e`**(那会改调用者的语义)。
|
||
# 调用者自己已经有 `set -uo pipefail`(`install.sh` 另有 `-e`),这里不重复声明。
|
||
|
||
# shellcheck shell=bash
|
||
|
||
# 记录本次兜了哪些变量(调用者可以打出来,见文件末尾的 `agentmail_env_report`)。
|
||
# 目的:**"某条脚本忘了 source" 目前没有信号** —— `AGENTMAIL_ENV_DEFAULTS` 只是被 export,
|
||
# 值本身不进正常输出。谁把 `source` 删了或写错位置,输出**少了那一行**才是可见信号;
|
||
# 否则"环境本来就齐"与"忘了 source"在输出上完全同形(又是"看起来在兜、其实没兜")。
|
||
AGENTMAIL_ENV_DEFAULTS=""
|
||
|
||
# ══════════════════════════════════════════════════════════════════════════════
|
||
# ★★ 环境预检的**相位标记**(pi 2026-09-15 提议;用的是本仓既有词汇)。
|
||
#
|
||
# 冲突原话(两条规则都对,撞在一起了):
|
||
# · **部署路径**:环境不足就该 fail-fast —— 这是本文件存在的理由;
|
||
# · **`install.sh --check`**:它的**产品就是诊断本身** ⇒ "只知道一件事"正是要避免的结果。
|
||
# 实测的坏结果(pi 报、我复现):③c 的 GOCACHE 预检在**开头** source 时 exit 2
|
||
# ⇒ `--check` **一条诊断都不跑**(输出 7 行、`========== test/` 命中 0 次)。
|
||
# 于是"别让第一道红吃掉后面的诊断"(我两轮前刚从 `CHECK_GATE_RC` 上修好的那条)
|
||
# **又以环境预检为入口重新成立了**,而被吃掉的恰是与 GOCACHE **毫无关系**的前端门禁。
|
||
#
|
||
# 口径(**逐条标记,不是整体开关**):每条预检自己声明"我拦得住的是哪一步"。
|
||
# · 只 gate 得了**某一步**(GOCACHE → go 那步)⇒ 标记为**可推迟**:记账、继续、
|
||
# 最后由调用者统一按**环境**退出(与 `CHECK_GATE_RC` 同形:先传出去、再跑完、最后按记录退出);
|
||
# · gate 得住**诊断本身**(HOME/TMPDIR 没了,"跑判据"这件事就不可信;外部命令缺了,套件根本起不来)
|
||
# ⇒ **不标记**,仍然立刻 exit 2。理由写在各自旁边。
|
||
#
|
||
# 这样"环境不足要说清"与"别让第一道红吃掉诊断"**各自都还在**。
|
||
# ⚠️ 变量必须在**任何预检之前**初始化为空 —— 否则第一条预检 append 到一个未定义变量上,
|
||
# 在 `set -u` 下会直接炸(而那会被读成"预检坏了",不是"环境不足")。
|
||
AGENTMAIL_ENV_DEFER_RECORD=""
|
||
AGENTMAIL_ENV_DEFERRED=0
|
||
AGENTMAIL_ENV_DEFER_KEYS=""
|
||
|
||
# 记一条"可推迟的环境问题"。
|
||
#
|
||
# ★ `agentmail_env_defer KEY MESSAGE`:**按 KEY 去重**(默认 KEY = 消息本身)。
|
||
# 为什么要去重:同一个**根因**会在两处各响一次 —— 例如 GOCACHE 不可写
|
||
# (③c 预检)与 `go vet` 因它跑不起来(`install.sh` 的分类步)。
|
||
# 不去重就会出现"环境不足(2 项)",而实际只有**一个**问题
|
||
# (我实测到了:报 2 项、两条说的是同一件事)—— 那会把一个根因读成两个,
|
||
# 而"数出来的问题个数"也是一种读数,读数错了就等于又造了一个假数字。
|
||
agentmail_env_defer() {
|
||
_am_key="${1:-}"
|
||
_am_msg="${2:-$1}"
|
||
case " $AGENTMAIL_ENV_DEFER_KEYS " in
|
||
*" $_am_key "*) return 0 ;; # 同一个根因已经记过 ⇒ 不重复计数
|
||
esac
|
||
AGENTMAIL_ENV_DEFERRED=$((AGENTMAIL_ENV_DEFERRED + 1))
|
||
AGENTMAIL_ENV_DEFER_KEYS="$AGENTMAIL_ENV_DEFER_KEYS $_am_key"
|
||
if [ -z "$AGENTMAIL_ENV_DEFER_RECORD" ]; then
|
||
AGENTMAIL_ENV_DEFER_RECORD="$_am_msg"
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
# 调用者(干跑路径)在诊断跑完后调用:把推迟的环境问题**统一报出来**并给退出码 **2**。
|
||
# 与"第一道红"的区别写在输出里,免得下一个人把它读成同一种东西。
|
||
agentmail_env_flush() {
|
||
if [ "${AGENTMAIL_ENV_DEFERRED:-0}" -eq 0 ]; then
|
||
return 0
|
||
fi
|
||
printf '\n [FAIL] 环境不足(%d 项)—— **这是环境问题,不是代码缺陷**:\n' "$AGENTMAIL_ENV_DEFERRED" >&2
|
||
printf '%s\n' "$AGENTMAIL_ENV_DEFER_RECORD" >&2
|
||
printf ' ★ 与"某道门红了"**不是同一件事**:这里报的是"**有一步跑不起来**"。\n' >&2
|
||
printf ' ★ 上面的诊断**已经全部跑完**(推迟到末尾正是为了这个 —— 环境预检在开头 exit 2\n' >&2
|
||
printf ' 会把与它无关的门一并吃掉;那是本仓修过一次的老毛病,入口换成了环境预检而已)。\n' >&2
|
||
printf ' ⇒ 按仓库约定用**退出码 2(环境)**。\n' >&2
|
||
return 2
|
||
}
|
||
|
||
# ③b 本文件**自己**依赖的命令(pi 评审 2026-09-15)。
|
||
#
|
||
# ★ 要害:这张表原先只登记了**调用者**的命令(`AGENTMAIL_REQUIRE`),而本文件自己用了
|
||
# `id`/`getent`/`cut`/`df`/`awk` **一个都没登记** —— "环境兜底自己还需要环境"。
|
||
# 实测(2026-09-15)三种后果,只有第一种是响的:
|
||
# · 缺 `id` ⇒ uid 误判 0 ⇒ 按 root 取 `/root` ⇒ 不可写 ⇒ 落到 `/tmp/agentmail-home-0`
|
||
# · 缺 `getent`/`cut` ⇒ 取不到 passwd 里的 home ⇒ 同上,**都不打 WARN**(静默改 HOME)
|
||
# · 缺 `df`/`awk` ⇒ `df -Pk … | awk` 拿到空串 ⇒ 走 `''|*[!0-9]*)` 那支 **"不知道,不判定"**
|
||
# ⇒ **②b 那一道空间门静默消失**(而它是一道**门**,不是提示)——最严重的一例
|
||
# ⇒ 与 ③b 那条"命令不在 ≠ 输出为空"的规矩**方向相同**:`df` 不在时应当走 2 + 人话,
|
||
# 而不是恰好落进"读不到 ⇒ 不判定"。
|
||
#
|
||
# 为什么登记在**本文件**而不是三个调用者的 `AGENTMAIL_REQUIRE` 里:那个变量在**调用点**赋值
|
||
# (`. env-defaults.sh` 在第 16/42/52 行,`AGENTMAIL_REQUIRE=` 在第 20/46/56 行),
|
||
# 也就是说 source 的时候它**还不存在** —— 所以本文件没法把它自己那份追加进调用者的表
|
||
# (追加了也不会在本次生效)。这里写成文件级的常量,与 `AGENTMAIL_REQUIRE` 一起过同一个检查。
|
||
AGENTMAIL_REQUIRE_SELF="id getent cut df awk"
|
||
|
||
# ④(**必须排在最前**,见文件头)PATH:只保证常见系统目录在(最小环境里 PATH 可能为空)。
|
||
case ":${PATH:-}:" in
|
||
*:/usr/bin:*) : ;;
|
||
*) PATH="${PATH:+$PATH:}/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||
export PATH
|
||
AGENTMAIL_ENV_DEFAULTS="$AGENTMAIL_ENV_DEFAULTS PATH=+系统目录" ;;
|
||
esac
|
||
|
||
# ③b-0 **本文件自己的依赖**:必须在下一次外部调用(① 的 `id`)**之前**检查。
|
||
#
|
||
# ★ 这里只用**内建命令**(`command -v` + `printf`),所以它能在"环境还什么都没兜"的时候跑;
|
||
# 而调用者那张表(`AGENTMAIL_REQUIRE`)要等 source 之后才知道,所以它仍在 ③b 处检查。
|
||
# 两次检查是**分工**不是重复:这次管本文件自己(顺序上必须在前),那次管调用者。
|
||
_am_self_missing=""
|
||
for _am_c in $AGENTMAIL_REQUIRE_SELF; do
|
||
command -v "$_am_c" >/dev/null 2>&1 || _am_self_missing="$_am_self_missing $_am_c"
|
||
done
|
||
if [ -n "$_am_self_missing" ]; then
|
||
printf '\n [FAIL] 环境不足:缺少命令%s(\n' "$_am_self_missing" >&2
|
||
printf ' 这是**env-defaults.sh 自己**用来兜底环境的命令,缺了它兜底会**静默降级**:\n' >&2
|
||
printf ' 缺 df/awk ⇒ 空间门静默消失;缺 id/getent ⇒ 静默改 HOME 且不报警。\n' >&2
|
||
printf ' 这是**环境问题**,不是代码问题。药方:装齐上述命令,或换一个有它们的环境。\n' >&2
|
||
exit 2
|
||
fi
|
||
unset _am_self_missing _am_c
|
||
|
||
# ① HOME:`go`/`npm`/`npx` 都会用它推默认缓存与配置位置。
|
||
#
|
||
# ★ **不能无脑给 `/root`**(pi 评审 2026-09-14):`redeploy-gateway.sh` 没有 EUID 断言,
|
||
# 于是在**非 root + 空 HOME** 下会拿到 `HOME=/root`,接下来的 `go build`/`npm ci`
|
||
# 往 `/root/go`、`/root/.npm` 写 ⇒ **EACCES**,而那串报错看起来是**代码/工程问题**
|
||
# —— 正是本文件存在的理由(把环境问题说成代码问题)。所以按身份分叉,**且验证可写**。
|
||
# ★ **`HOME` 已给也要验可写**(pi 的思路我扩了一步,实测发现的第二个口子):
|
||
# 非 root 调用者若带着 `HOME=/root`(sudo -E、从 root shell 继承、容器里挂错)过来,
|
||
# 只判"未设"就放行 ⇒ 后面 `go build`/`npm ci` 写 `/root/go`、`/root/.npm` ⇒ **EACCES**,
|
||
# 报错看起来是代码问题 —— 与"空 HOME"是**同一个后果**,只是入参不同。
|
||
# 实测:`setpriv --reuid=65534 env -i HOME=/root` ⇒ `touch $HOME/probe` 被拒。
|
||
# 以 **root** 跑时 `/root` 写得进去,所以这条只在非 root 下触发 ——
|
||
# 判据落在"**能不能写**",不落在"路径长得像不像"。
|
||
#
|
||
# ★★ 但"验证可写"**不能用 `[ -w ]`**(dsh 实测 2026-09-25,修本文件自己的一个假绿):
|
||
# `[ -w ]` 查的是**权限位**。以 uid=0 跑时它对任何 root 属主的目录都返回**真**,
|
||
# 而**实际拦截可能在另一层**。本机实测(同一台、同一 shell):
|
||
# [ -w /root ] ⇒ true
|
||
# touch /root/x ⇒ Permission denied ← 能写才算数
|
||
# [ -w /opt/agentmail ] ⇒ true
|
||
# touch /opt/agentmail/x ⇒ Permission denied
|
||
# (本会话每次 shell 都带着 `landlock-run: partial enforcement` —— 拦截在沙箱层,
|
||
# 权限位那层**看不出来**。)
|
||
# ⇒ 后果正是本文件要防的那件事,而且更绕:`_am_home=/root` 被**误判为可用** ⇒
|
||
# `HOME=/root` 放行 ⇒ ② 段拿 `$HOME/.cache/go-build` 当 GOCACHE ⇒ 那里用的是
|
||
# **真写探针**(297 行)⇒ 探针失败 ⇒ 报出**「GOCACHE 不可写」**。
|
||
# ⇒ 根因是 HOME,报出来的是 GOCACHE —— **误指了对象**,与本文件存在的理由正好相反。
|
||
# ⇒ 所以这里也改用**真写探针**(与 297 行同一个办法,不再一处真写、一处只看权限位)。
|
||
_am_can_write() {
|
||
[ -n "${1:-}" ] || return 1
|
||
[ -d "$1" ] || return 1
|
||
{ : > "$1/.agentmail-wprobe-$$"; } 2>/dev/null || return 1
|
||
rm -f "$1/.agentmail-wprobe-$$"
|
||
return 0
|
||
}
|
||
_am_uid="$(id -u 2>/dev/null || echo 0)"
|
||
_am_home_given=0
|
||
if [ -z "${HOME:-}" ]; then
|
||
if [ "$_am_uid" = "0" ]; then
|
||
_am_home="/root"
|
||
else
|
||
# 非 root:从 passwd 库取该 uid 的 home(比猜 /root 或 /home/$USER 准)
|
||
_am_home="$(getent passwd "$_am_uid" 2>/dev/null | cut -d: -f6 || true)"
|
||
fi
|
||
else
|
||
_am_home_given=1
|
||
_am_home="$HOME"
|
||
fi
|
||
# 目录必须落在**调用者写得进去**的地方,否则就是把问题往后推。
|
||
# 注意:`mkdir -p` 对已存在的不可写目录会**返回成功**,所以必须再单独判可写 ——
|
||
# 而那个"单独判"**必须真去写**(见上面 ★★:`[ -w ]` 在沙箱/ACL 那一层会假绿)。
|
||
_am_fallback="${TMPDIR:-/tmp}/agentmail-home-$_am_uid"
|
||
if [ -z "$_am_home" ] || [ ! -d "$_am_home" ] || ! _am_can_write "$_am_home"; then
|
||
if [ "$_am_home_given" = "1" ] && [ -n "$_am_home" ]; then
|
||
# 调用者显式给了 HOME 却不可写:**说清楚**再改判,不静默换目录
|
||
# (静默改 HOME 会让"东西写到哪去了"变成一个谜)。
|
||
printf ' [WARN] 调用者给的 HOME=%s 不可写(非 root 且非该用户所有);改判到 %s\n' \
|
||
"$_am_home" "$_am_fallback" >&2
|
||
else
|
||
# ★ **兜底自己被环境打败**这条路原先没有信号(pi 评审 2026-09-15):
|
||
# 调用者没给 HOME,而"按身份推出来的那个"也不可用(root 的 /root 在非 root 下不可写;
|
||
# 或 passwd 里的 home 不存在,例如 nobody 的 /nonexistent)。
|
||
# 原先只报"调用者给的不可写",于是这条**悄悄换了 HOME**:
|
||
# 东西写到哪去了变成一个谜 —— 与本文件存在的理由(把环境问题说清楚)正好相反。
|
||
#
|
||
# ★ 括注原写"(非 root / passwd 里没有可用目录)"—— 那是**按 uid 猜的原因**,
|
||
# 而判据已改成**真写探针**(见上 ★★)⇒ 现在 uid=0 也可能走到这里
|
||
# (能写与否取决于沙箱/ACL,不取决于身份)。**不要再报一个没验过的原因**:
|
||
# 只报"写不进去"这个已测事实,并附上身份供人判断。
|
||
#
|
||
# ⚠️ 文案前半句 `按身份推出来的 HOME=%s 不可用` **是 pi 的判据匹配的锚**
|
||
# (`plugins/pi-mail-bridge/test/env-guard.test.mjs:268` 用 assert.match 锁它,
|
||
# 理由是"兜底路径改了 HOME 就必须留痕")。⇒ **锚不许动**,只把括注改准。
|
||
printf ' [WARN] 按身份推出来的 HOME=%s 不可用(已实测真写探针;uid=%s);改判到 %s\n' \
|
||
"${_am_home:-(空)}" "$_am_uid" "$_am_fallback" >&2
|
||
fi
|
||
_am_home="$_am_fallback"
|
||
fi
|
||
if mkdir -p "$_am_home" 2>/dev/null && _am_can_write "$_am_home"; then
|
||
if [ "$_am_home" != "${HOME:-}" ]; then
|
||
HOME="$_am_home"
|
||
export HOME
|
||
AGENTMAIL_ENV_DEFAULTS="$AGENTMAIL_ENV_DEFAULTS HOME=$HOME"
|
||
fi
|
||
else
|
||
# 连一处可写的 HOME 都找不到 ⇒ **这是环境不足**,按约定 exit 2,
|
||
# 并且用人话说明"不要把它当代码问题"(调用者的 go/npm 报错会更晦涩)。
|
||
printf '\n [FAIL] 环境不足:找不到可写的 HOME(试过 %s)\n' "$_am_home" >&2
|
||
printf ' 这是**环境问题**,不是代码问题。药方:在可写的环境里重跑,或显式给 HOME=<可写目录>。\n' >&2
|
||
exit 2
|
||
fi
|
||
|
||
# ② TMPDIR:构建暂存与测试都往这里写。缺省 /tmp 可能与真实挂载不符,
|
||
# 但"空的 TMPDIR"比"指错一个已存在的目录"更糟 —— 后者会让判据看错地方。
|
||
if [ -z "${TMPDIR:-}" ]; then
|
||
TMPDIR=/tmp
|
||
export TMPDIR
|
||
AGENTMAIL_ENV_DEFAULTS="$AGENTMAIL_ENV_DEFAULTS TMPDIR=$TMPDIR"
|
||
fi
|
||
|
||
# ③ GOMODCACHE / GOPATH:go 的模块缓存。**只在 go 存在时**推导,
|
||
# 且只在两者都空的时候推导(有 GOPATH 时 go 自己会算 GOMODCACHE)。
|
||
#
|
||
# ⚠️ 这里的 `command -v` 依赖 ④ 已经跑过(PATH 就位)—— 见文件头的顺序说明。
|
||
if command -v go >/dev/null 2>&1; then
|
||
if [ -z "${GOMODCACHE:-}" ] && [ -z "${GOPATH:-}" ]; then
|
||
GOPATH="$HOME/go"
|
||
export GOPATH
|
||
AGENTMAIL_ENV_DEFAULTS="$AGENTMAIL_ENV_DEFAULTS GOPATH=$GOPATH"
|
||
fi
|
||
fi
|
||
|
||
export AGENTMAIL_ENV_DEFAULTS="${AGENTMAIL_ENV_DEFAULTS# }"
|
||
|
||
# ③c `GOCACHE`(go 的 **build** 缓存)—— pi 评审 2026-09-15 指出的**缺项**。
|
||
#
|
||
# ★ 上面 ③ 预检的是 `GOMODCACHE`/`GOPATH`(**模块**缓存),而 `go vet`/`go test`/`go build`
|
||
# 还要用 **build** 缓存。缺了/不可用时的报错长这样:
|
||
#
|
||
# internal/middleware/auth.go:9:2: open /root/.cache/go-build/55/559b2f74…-d: permission denied
|
||
#
|
||
# —— 它**指着源码文件**说 permission denied,看起来完全像代码缺陷;而 `install.sh` 那一步
|
||
# 接着打 `[FAIL] go vet / go test 不过 —— **先修好再安装**` ⇒ 把人送去改代码。
|
||
# pi 一条命令就把它分开了:`go vet ./internal/repo/` 通过、`go vet ./...` 死在缓存条目上
|
||
# ⇒ 真因是 build 缓存不可用,**不是任何 .go 文件有问题**。
|
||
# **这正是本文件上面第 25 行那条纪律要拦的东西**:
|
||
# "失败要**说清是环境问题**…**不要让它冒充代码缺陷**"。
|
||
#
|
||
# ★ 为什么必须**实测可写**而不是只判 `-d`:build cache 是**要写**的(未命中就写条目)。
|
||
# 只判存在 ⇒ 目录在就放行,go 一写就 EACCES —— 本仓那条老毛病
|
||
# (见 ① HOME 的注释:"对已存在的不可写目录,`mkdir -p` 会返回成功 ⇒ 必须单独判 `-w`")。
|
||
#
|
||
# ⚠️ `go env GOCACHE` 可能返回字面量 `off`(实测:本机沙箱下就是 `off`)——
|
||
# 那**不是目录**,按"拿不到可用的 build 缓存"处理,**不要**把 `off` 读成"用户特意关掉了、
|
||
# 所以放行":此时 go 确实用不了缓存,报错照样会冒充代码缺陷。
|
||
if command -v go >/dev/null 2>&1; then
|
||
# 口径与本文件一致(**只设未设的**):GOCACHE 已给就用它,没给才推导。
|
||
_am_gocache="${GOCACHE:-}"
|
||
if [ -z "$_am_gocache" ]; then
|
||
# 与 go 自己的默认同式:`$XDG_CACHE_HOME` 优先,否则 `$HOME/.cache`。
|
||
# ⚠️ **不能只靠 `go env GOCACHE`**:本机实测它在沙箱里返回字面量 `off`
|
||
# (HOME 对 go 不可用时的表现),而 `HOME`/`XDG_CACHE_HOME` 都可用时它返回正常路径。
|
||
_am_gocache="${XDG_CACHE_HOME:-$HOME/.cache}/go-build"
|
||
GOCACHE="$_am_gocache"
|
||
export GOCACHE
|
||
AGENTMAIL_ENV_DEFAULTS="$AGENTMAIL_ENV_DEFAULTS GOCACHE=$GOCACHE"
|
||
fi
|
||
# ⚠️⚠️ **必须判"是绝对路径"** —— 这是我实测踩到的:`go env GOCACHE` 在本沙箱里返回
|
||
# 字面量 `off`,而我第一版直接 `mkdir -p "$_am_gocache"` ⇒ **在当前目录里造出一个 ./off**!
|
||
# 探针去写一个相对路径必然"成功" ⇒ 判据**假绿**,而且**留下副作用**
|
||
# ("预检"自己改了工作树 —— 正是本仓刚从 `dist`/`gen:bg` 上修掉的那一类)。
|
||
# ⇒ 非绝对路径**不可能是** go 真正会用的缓存目录 ⇒ 按"拿不到可用缓存"处理。
|
||
case "$_am_gocache" in
|
||
/*) : ;;
|
||
*)
|
||
_am_gc_msg="GOCACHE 不是绝对路径(实得「${_am_gocache}」)——
|
||
本机实测:沙箱里 \`go env GOCACHE\` 会给出字面量 \`off\`,那表示它用不了 build 缓存。
|
||
药方:GOCACHE=<可写目录的绝对路径> 重跑。"
|
||
if [ "${AGENTMAIL_ENV_DEFER:-0}" = "1" ]; then
|
||
agentmail_env_defer "gocache" "$_am_gc_msg"
|
||
else
|
||
printf '\n [FAIL] 环境不足:%s\n' "$_am_gc_msg" >&2
|
||
printf ' 这是**环境问题**,不是代码问题。\n' >&2
|
||
exit 2
|
||
fi
|
||
;;
|
||
esac
|
||
mkdir -p "$_am_gocache" 2>/dev/null || true
|
||
# 真的写一个探针文件 —— 与 go 自己要做的事**同形**(不只看权限位;root 无视权限位,`-w` 骗人)。
|
||
#
|
||
# ⚠️⚠️ **不要用 `set -e` 做这件事**(我第一版就是这么写的,**探针恒绿**):
|
||
# `if ! ( set -e; … )` 里那个子壳是**条件的一部分**,而 bash 在条件上下文里
|
||
# **抑制 errexit** ⇒ 写失败也不中止 ⇒ 子壳最后一条命令(`rm -f`)返回 0 ⇒
|
||
# 判据永远说"能写"。实测:`GOCACHE=/proc/self/nonexistent-gc` 时
|
||
# `mkdir` 与重定向**都报 No such file or directory**,而探针 rc=0。
|
||
# ⇒ 用**显式**的 `|| exit 1`,把失败直接变成退出码,不依赖 shell 选项的上下文语义。
|
||
#
|
||
# ★ 这里标**可推迟**(`AGENTMAIL_ENV_DEFER`):GOCACHE 只 gate 得了**go 那一步**,
|
||
# 前端门禁(typecheck / 判据 / build)与它**毫无关系** ⇒ 干跑里没必要因为它全停。
|
||
if ! ( { : > "$_am_gocache/.agentmail-probe"; } 2>/dev/null || exit 1; rm -f "$_am_gocache/.agentmail-probe" ); then
|
||
_am_gc_msg="GOCACHE=${_am_gocache} 不存在或不可写 —— 症状(会冒充代码缺陷):
|
||
\`go vet ./...\` 报 internal/…: open ${_am_gocache}/…-d: permission denied,
|
||
它指着源码文件说 permission denied,而实际一行 .go 都没问题。
|
||
药方:GOCACHE=<可写目录> 重跑(或修 ${_am_gocache} 的权限)。"
|
||
if [ "${AGENTMAIL_ENV_DEFER:-0}" = "1" ]; then
|
||
agentmail_env_defer "gocache" "$_am_gc_msg"
|
||
else
|
||
printf '\n [FAIL] 环境不足:%s\n' "$_am_gc_msg" >&2
|
||
printf ' 这是**环境问题**,不是代码问题。\n' >&2
|
||
exit 2
|
||
fi
|
||
fi
|
||
unset _am_gocache _am_gc_msg
|
||
fi
|
||
|
||
# ②b TMPDIR **可写 + 有空间** —— 与 ① HOME 同一条规则,必须一起落。
|
||
#
|
||
# ★ pi 评审 2026-09-14:① 那边我写了"`mkdir -p` 对已存在的不可写目录会返回成功 ⇒ 必须单独判 `-w`",
|
||
# **同一条规则没落到 TMPDIR 上** —— 而 ENOSPC 正是这条链的元老问题(四次史里第 3 条就是它)。
|
||
# 两种失败形状,后果都是在**中间**炸、报错看起来像工程问题:
|
||
# · `TMPDIR` 已给但**不可写**(`sudo -E`、容器里挂错、只读挂载)⇒ EACCES
|
||
# · `TMPDIR` 可写但**已满**(2026-09-14 实测 `bavail` 真是 0)⇒ `-w` 也抓不到,要的是**空间**判定
|
||
# 所以:`-d` + `-w`,再判可用空间;不足就人话 + exit 2。
|
||
#
|
||
# 口径(与 `plugins/pi-mail-bridge/test/lib/tmp-space.mjs` 一致,但**不 import 它**:
|
||
# `deploy/` 侧要能独立分发,为去重引进平台代码不划算 —— 这里用最小实现 + `df`):
|
||
# · 读不到 ⇒ **不据此判定**("不知道 ≠ 不对",否则在不认识的文件系统上制造恒红的判据);
|
||
# · `0` 是**真的没有**,不是"不知道";
|
||
# · 实测本机 `/tmp` 是 tmpfs、曾 100% 占满(可用 0),连 `go build` 都进不去。
|
||
if [ ! -d "${TMPDIR:-}" ] || [ ! -w "${TMPDIR:-}" ]; then
|
||
printf '\n [FAIL] 环境不足:TMPDIR=%s 不存在或不可写\n' "${TMPDIR:-(空)}" >&2
|
||
printf ' 这是**环境问题**,不是代码问题(构建/测试写临时文件时会以 EACCES 炸在中间)。\n' >&2
|
||
printf ' 药方:TMPDIR=<可写目录> 重跑。\n' >&2
|
||
exit 2
|
||
fi
|
||
# 可用空间:`df -Pk` 第 4 列是可用 KB。读不到就跳过(不判定)。
|
||
_am_avail_kb="$(df -Pk "$TMPDIR" 2>/dev/null | awk 'NR==2 {print $4}')"
|
||
case "${_am_avail_kb:-}" in
|
||
''|*[!0-9]*) : ;; # 读不到/非数字 ⇒ 不知道,不判定
|
||
*)
|
||
if [ "$_am_avail_kb" -le 0 ]; then
|
||
printf '\n [FAIL] 环境不足:TMPDIR=%s 可用空间为 0\n' "$TMPDIR" >&2
|
||
printf ' 这是**环境问题**(本机 2026-09-14 实测过 tmpfs 占满,连 go build 都进不去)。\n' >&2
|
||
printf ' 药方:清理 %s 或改 TMPDIR=<别的可写目录>。\n' "$TMPDIR" >&2
|
||
exit 2
|
||
fi
|
||
AGENTMAIL_TMP_AVAIL_KB="$_am_avail_kb"
|
||
;;
|
||
esac
|
||
unset _am_avail_kb
|
||
|
||
# ③b **我依赖的命令**:环境自足原先只覆盖"变量",没覆盖"命令"。
|
||
#
|
||
# ★ pi 评审 2026-09-14 给出的要害判据(我认,而且这是这条链最容易假绿的地方):
|
||
# **"命令不在"必须走 2(或红)+ 人话;"命令在但输出为空"才是判定结果。**
|
||
# 现在 `redeploy-gateway.sh` 那两处把两者压成了同一个字符串 `"0"`:
|
||
# fc="$(journalctl … 2>/dev/null | grep -icE 'panic|fatal|SIGSEGV' || true)"
|
||
# [ "${fc:-0}" = "0" ] && ok "近 2 分钟无 panic/fatal" ← **一条假绿**
|
||
# journalctl 不在 / 无权限读日志时,它失败(被 `2>/dev/null` 吞掉)⇒ grep 读空输入
|
||
# ⇒ 输出 0、退出 1 ⇒ `|| true` ⇒ `fc="0"` ⇒ 报"无 panic"。
|
||
# 实测复现:`journalctl -u 不存在的-unit` ⇒ `fc=[0]` ⇒ 会打印 [ OK ]。
|
||
# 下面 `sse` 那条同形 ⇒ 报"暂未看到 SSE 重连 —— 检查密钥是否被撤销",
|
||
# **把"工具缺失"归因成"插件没连上"**(人去查密钥,而问题在 journalctl)。
|
||
#
|
||
# ⚠️ 顺带实测:**`PIPESTATUS` 分不开这两种情况**(命令不存在与"存在但无匹配"都给 `1`),
|
||
# 所以不能靠管道状态区分 —— 必须显式做前提检查。这就是这一节存在的理由。
|
||
#
|
||
# 调用者声明自己那组(`AGENTMAIL_REQUIRE`,空格分隔),缺一个就 exit 2。
|
||
# 与四次史的处理**同形**,只是对象从变量换成了命令。
|
||
#
|
||
# ★ **新增任何外部命令时,回到调用者的 `AGENTMAIL_REQUIRE` 登记**(pi 评审 2026-09-14)。
|
||
# 这条规矩不是形式主义,有一个我自己刚踩的实例:我加了"同时性"那一列(部署锁)、
|
||
# 引入了新命令 `flock`,**却没回到这张表登记**。后果实测:
|
||
# PATH 里没有 flock ⇒ `flock: command not found`(127)⇒ `! flock` 为真
|
||
# ⇒ 打印"**另一个部署正在跑(锁被占用)**" —— 退出码事后是对的(2),
|
||
# 但**诊断是错的**,而照着它做的是"等另一个部署结束":**永远等不到**。
|
||
# 即"**表与被表的东西不同步**":新能力自带的依赖没有登记回表。
|
||
# 所以每次给部署路径加一个新命令,都要回来加一个词。
|
||
# ★★ 相位缺陷(pi 2026-09-25 实测报回,dsh 独立复核成立 —— 本文件自己的一个大假绿):
|
||
# 上面这段**原先只在 source 期执行**,而三个调用者**都是先 source 后赋值**:
|
||
# install.sh source 131 / 赋值 135
|
||
# redeploy-gateway.sh source 42 / 赋值 46
|
||
# redeploy-plugin.sh source 52 / 赋值 56
|
||
# ⇒ source 那一刻 `AGENTMAIL_REQUIRE` 还是**空的** ⇒ **整张表从未被检查过**。
|
||
# dsh 实测 A/B(本机,同一台同一 shell):
|
||
# `AGENTMAIL_REQUIRE=__no_such_cmd . deploy/lib/env-defaults.sh` ⇒ rc=2(能抓到)
|
||
# `. deploy/lib/env-defaults.sh; AGENTMAIL_REQUIRE=__no_such_cmd` ⇒ rc=0(**漏过**)
|
||
# ⇒ 这正是本文件头反复记的那件事落在**本文件自己**身上:**判据在,但走不到**。
|
||
# ⇒ 修法不是"让三个调用者改相位"(要改三处,且下一个新调用者还会犯),
|
||
# 而是把检查**同时**挂到"表被赋值之后必然经过的那一步":`agentmail_env_report`
|
||
# (三个调用者都在赋值后**立刻**调它)。两处都留,因为两条路都要接住:
|
||
# · source 期那次 —— 接"表由**环境**提供"(父进程 export / CI 注入)
|
||
# · 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
|
||
for _am_c in $AGENTMAIL_REQUIRE; do
|
||
command -v "$_am_c" >/dev/null 2>&1 || _am_missing="$_am_missing $_am_c"
|
||
done
|
||
if [ -n "$_am_missing" ]; then
|
||
printf '\n [FAIL] 环境不足:缺少命令%s\n' "$_am_missing" >&2
|
||
printf ' 这是**环境问题**,不是代码问题。("命令不在"与"命令在但输出为空"必须分开:\n' >&2
|
||
printf ' 前者会让下游把"工具缺失"读成"检查通过"。)\n' >&2
|
||
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` 若由
|
||
# **环境**(父进程 export / CI 注入)提供,source 期那次就能看到;若由**调用者赋值**,
|
||
# 则靠 `agentmail_require` 动作或本行这次。三条路都接住。
|
||
agentmail_env_check_require
|
||
if [ -n "$AGENTMAIL_ENV_DEFAULTS" ]; then
|
||
printf ' 环境兜底:%s\n' "$AGENTMAIL_ENV_DEFAULTS"
|
||
else
|
||
printf ' 环境兜底:(无 —— 调用者已提供全部)\n'
|
||
fi
|
||
}
|
||
|
||
|