Files
MailUI4Agents/deploy/lib/env-defaults.sh
JianFeeeee 633150cf39 修复: go vet 的环境失败不许冒充代码缺陷——env-defaults 补 GOCACHE 预检(缺项)+ 安装脚本按输出分类,命中缓存/权限/空间走退出码 2
pi 2026-09-15 指出的是**报文**的问题,而规矩本来就在 `lib/env-defaults.sh:25`:
"失败要**说清是环境问题**并按仓库约定用退出码 2(环境),**不要让它冒充代码缺陷**"。

## 一、缺项:③ 预检了**模块**缓存,没预检 **build** 缓存

```
$ go vet ./...   ⇒ internal/middleware/auth.go:9:2: open /root/.cache/go-build/55/…-d: permission denied
                   [FAIL] go vet / go test 不过 —— **先修好再安装**
```

报错**指着源码文件**说 permission denied,跟着那句话把人送去改代码 —— 而一行 `.go` 都没问题。

**处置(两道)**:

1. `env-defaults.sh` 新增 ③c:**GOCACHE 实测可写**(写一个探针文件,不只看权限位 ——
   root 无视权限位,`-w` 会骗人),不可用 ⇒ 人话 + **exit 2**;
2. `install.sh` 那一步按**输出分类**:命中 `permission denied` / `module cache not found` /
   `no space left` / `GOCACHE` ⇒ **退出码 2(环境)**、并且**不再说"先修好"**;
   没命中 ⇒ 仍按代码缺陷(退出码 1),报文里**明说**是哪种。

**实测(分类逻辑逐字节抽出、只替换"怎么产生输出")**:

| 喂进的输出 | 退出码 |
|---|---|
| `… open /root/.cache/go-build/55/x-d: permission denied` | **2** |
| `go: module cache not found: neither GOMODCACHE nor GOPATH is set` | **2** |
| `./internal/repo/x.go:12:2: undefined: Foo` | **1** |
| `--- FAIL: TestBar` | **1** |

## 二、★ 实现过程中两次把自己的判据写成"永远不会响",都是**实测**抓到的

这两条我记下来,因为它们是同一个形状、而且**都在我手上当天发生**:

**① 先 `rm` 再 `grep`** —— 第一版我写成

```
cat "$GOOUT" >&2
rm -f "$GOOUT"                              # ← 先删
if grep -qE 'permission denied|…' "$GOOUT"; then   # ← 再读,永远读不到
```

⇒ 分类**永远不触发**,所有失败都会被报成代码缺陷(**恰好是 pi 报的那个问题原样保留**)。
改成"先分类、再删"。

**② `set -e` 在 `if ! ( … )` 里被**抑制** —— 探针**恒绿**:

```
if ! ( set -e; : > "$GOCACHE/.agentmail-probe"; rm -f … ); then …
```

实测 `GOCACHE=/proc/self/nonexistent-gc` 时 `mkdir` 与重定向**都报 No such file or directory**,
而探针 **rc=0** ⇒ 判据说"能写"。真因:bash 在**条件上下文**里抑制 `errexit`,
于是子壳不中止、最后一条命令(`rm -f`)返回 0。
⇒ 改成**显式** `|| exit 1`,不依赖 shell 选项在特定上下文里的语义。

**③ 附带:我的预检自己造了一个目录** —— 第一版直接 `mkdir -p "$_am_gocache"`,
而 `go env GOCACHE` 在本沙箱里返回字面量 `off` ⇒ **在当前目录里创建了 `./off`**,
探针写相对路径必然"成功" ⇒ **假绿 + 副作用**("预检"改了工作树,正是刚从 `dist`/`gen:bg` 上
修掉的那一类)。已清掉(`rmdir`),并加了"**必须是绝对路径**,否则 exit 2"。
口径也改成**不依赖 `go env GOCACHE`**:`${GOCACHE:-${XDG_CACHE_HOME:-$HOME/.cache}/go-build}`
(与本文件"只设未设的"一致),`go env` 只作为旁证。

## 三、实测:本机这个缺项是**真的在响**

```
GOCACHE=(未设)  ⇒ rc=2
GOCACHE=off       ⇒ rc=2
GOCACHE=./rel     ⇒ rc=2
GOCACHE=/proc/self/nope ⇒ rc=2
GOCACHE=/tmp/gc-ok ⇒ rc=0
```

而且它**不是**误报 —— 直接问 go 是最权威的:

```
$ GOCACHE=/root/.cache/go-build go build ./internal/repo/
open /root/.cache/go-build/a4/…-d: permission denied
```

⇒ 本机 go 确实写不了 build 缓存 ⇒ precheck 判环境、**exit 2** 是对的。
(`HOME` 未设时 env-defaults 会兜成 `/root`,所以推导出的正是 go 自己会用的那个路径。)

## 四、端到端(`--check` + 可写 GOCACHE)

```
$ GOCACHE=/tmp/gc-ok bash deploy/install.sh --check      → 退出码 1
    [FAIL] 前端门禁没过(退出码 1)—— 上面标了 [FAIL] 的那一处就是
    [ OK ] go vet + go test 通过
    + exit 1                     ← bash -x 追到终点那一行
```

- `go vet` 变绿(缓存可写)⇒ 说明 ③c **不是**无条件拦人,只在真不可用时拦;
- 退出码 1 来自**前端门禁那 3 条真红**,不是环境;
- 干跑仍然零副作用:`git status` + `dist` 全量 sha 的指纹**前后一致** ✓。

★ 照实写的取舍:`--check` 现在在 GOCACHE 不可用时会**提前 exit 2**,
于是"诊断全部跑完"那段不会执行。这是**有意的** —— 按仓库约定环境问题用 2 且要早说,
而"跑不完的诊断"本身是环境问题的症状。不是"第一道红吃掉诊断"那个老毛病(那条已修)。
2026-09-15 14:03:17 +08:00

325 lines
20 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=""
# ③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
/*) : ;;
*)
printf '\n [FAIL] 环境不足:GOCACHE 不是绝对路径(实得「%s」)\n' "$_am_gocache" >&2
printf ' 这是**环境问题**,不是代码问题。\n' >&2
printf ' 本机实测:沙箱里 `go env GOCACHE` 会给出字面量 `off` —— 那表示**它用不了 build 缓存**。\n' >&2
printf ' 药方:GOCACHE=<可写目录的绝对路径> 重跑。\n' >&2
exit 2
;;
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 选项的上下文语义。
if ! ( { : > "$_am_gocache/.agentmail-probe"; } 2>/dev/null || exit 1; rm -f "$_am_gocache/.agentmail-probe" ); then
printf '\n [FAIL] 环境不足:GOCACHE=%s 不存在或不可写\n' "$_am_gocache" >&2
printf ' 这是**环境问题**,不是代码问题。\n' >&2
printf ' 症状(会冒充代码缺陷):`go vet ./...` 报\n' >&2
printf ' internal/…: open %s/…-d: permission denied\n' "$_am_gocache" >&2
printf ' —— 它指着源码文件说 permission denied,而实际一行 .go 都没问题。\n' >&2
printf ' 药方:GOCACHE=<可写目录> 重跑(或修 %s 的权限)。\n' "$_am_gocache" >&2
exit 2
fi
unset _am_gocache
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
}