Files
MailUI4Agents/deploy/lib/env-defaults.sh
JianFeeeee 3c8f18d4de 修复: 环境预检标相位——干跑里只 gate 得住某一步的预检改为"记账并继续、末尾按环境 exit 2",不再让第一道环境红吃掉全部诊断
pi 2026-09-15 报的是**我上一轮修复的代价**,而它是我两轮前刚从 `CHECK_GATE_RC` 上修好的
**同一个病**、只换了个入口。

## 一、病:`--check` 一条诊断都不跑

```
$ bash deploy/install.sh --check   → EXIT=2,输出 7 行,`========== test/` 命中 0 次
```

`env-defaults.sh` 在**开头** source,③c 的 GOCACHE 预检在那里 `exit 2`
⇒ 整个干跑在第一步就结束。被吃掉的正好是与 GOCACHE **毫无关系**的前端门禁
(typecheck / 判据 / build)—— 而"别让第一道红吃掉后面的诊断"是我两轮前刚立的性质。

## 二、修法:逐条标相位(不是整体开关)

两条规则都对、撞在一起:**部署路径**要 fail-fast;**`--check`** 的产品**就是诊断本身**。
所以按"这条预检拦得住的是哪一步"**逐条标记**:

| 预检 | 拦得住什么 | 相位 |
|---|---|---|
| ③c `GOCACHE` | 只 gate 得了 **go 那一步** | **可推迟**(记账 → 继续 → 末尾 exit 2)|
| ②b `TMPDIR` 可写/空间 | 构建与测试都要写它 | 可推迟(未标,留给下一轮按同一形状处置)|
| ① `HOME`、② `TMPDIR` 存在、③b 外部命令 | **诊断本身** | **不标,仍立刻 exit 2** |

推迟后由 `agentmail_env_flush` 在**诊断跑完后**统一报出并给退出码 **2**,
报文里写明"这与**某道门红了**不是同一件事"。
**部署路径行为完全不变**(实测:同一环境下 6 行、exit 2、0 条诊断)。

## 三、四象限实测(同一台机,只改 `GOCACHE`)

| 路径 | 环境 | 退出码 | 诊断文件数 |
|---|---|---|---|
| `--check` | 坏 | **2** | **23**(原先 0)|
| `--check` | 好 | **1**(前端 3 真红) | 23 |
| 部署 | 坏 | **2** | 0(fail-fast,不变) |

`--check` 零副作用仍成立(`git status` + `dist` 全量 sha 指纹前后一致)。

## 四、★ 实现过程中又两次"判据在,但走不到"——都是我实测抓的

**① 相位标记设在了 `source` 之后。** 我第一版把 `AGENTMAIL_ENV_DEFER=1` 放在原来的
参数解析处(第 24 行附近),而 `CHECK_ONLY` 在那里**还没赋值** ⇒ 条件为假 ⇒ **标记根本没设上**;
更坏的是**输出看不出区别**(照样一条诊断都不跑)。真因:标记是给 **source 期间**跑的预检看的,
而 `env-defaults.sh` 在 source 时就跑完了预检 ⇒ **参数解析必须整段挪到 source 之前**。
⇒ 已把解析块上移到 `REPO=` 之后、`source` 之前。

**② 末尾的 flush 放在前端门禁的 early-return 之后。** 于是"环境不足 + 前端门禁红"时
它**永远不会执行**(实测:退出码 2、末尾环境报文一行都没有)。已移到 `exit "${CHECK_GATE_RC:-0}"` 之前。

**③ 又一例同族:分类代码自己 `rm` 了要 grep 的文件** —— 上一轮已修,这轮复述在注释里当参照。

## 五、去重(这条是被实测数字逼出来的)

第一次跑通后报文说"环境不足(**2 项**)",而实际只有**一个**根因:
GOCACHE 不可写 会响两次 —— ③c 的预检、以及 `go vet` 因它跑不起来(`install.sh` 的分类步)。
⇒ `agentmail_env_defer KEY MESSAGE` 改成**按 KEY 去重**,两处共用 KEY `gocache`。
理由写进代码:**"数出来的问题个数"也是一种读数,读数错了就是又造了一个假数字。**

## 六、状态

build `checks=401 pass=400 fail=1 red=4`;install `389/389 fail=0 red=3`;`criteria-hygiene` 6/6。
2026-09-15 14:12:48 +08:00

400 lines
25 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 下触发 ——
# 判据落在"**能不能写**",不落在"路径长得像不像"。
_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`。
_am_fallback="${TMPDIR:-/tmp}/agentmail-home-$_am_uid"
if [ -z "$_am_home" ] || [ ! -d "$_am_home" ] || [ ! -w "$_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**:
# 东西写到哪去了变成一个谜 —— 与本文件存在的理由(把环境问题说清楚)正好相反。
printf ' [WARN] 按身份推出来的 HOME=%s 不可用(非 root / passwd 里没有可用目录);改判到 %s\n' \
"${_am_home:-(空)}" "$_am_fallback" >&2
fi
_am_home="$_am_fallback"
fi
if mkdir -p "$_am_home" 2>/dev/null && [ -w "$_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),
# 但**诊断是错的**,而照着它做的是"等另一个部署结束":**永远等不到**。
# 即"**表与被表的东西不同步**":新能力自带的依赖没有登记回表。
# 所以每次给部署路径加一个新命令,都要回来加一个词。
if [ -n "${AGENTMAIL_REQUIRE:-}" ]; then
_am_missing=""
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
unset _am_missing _am_c
fi
# 调用者打一行,让"兜了什么"**可见**(也是"忘了 source"的信号 —— 少了这行就不对)。
agentmail_env_report() {
if [ -n "$AGENTMAIL_ENV_DEFAULTS" ]; then
printf ' 环境兜底:%s\n' "$AGENTMAIL_ENV_DEFAULTS"
else
printf ' 环境兜底:(无 —— 调用者已提供全部)\n'
fi
}