Files
MailUI4Agents/deploy/lib/env-defaults.sh
JianFeeeee 3be824849d fix(deploy): env-defaults 的顺序错(③ 的探测依赖 ④ 的产物)+ 非 root 的 HOME 陷阱 + 兜底行可见 + 补 root 断言
pi 读了新加的 `deploy/lib/env-defaults.sh`(三个 source 点他都确认对),指出三条,逐一实测后处理:

**1. 顺序错(真错,而且正好落在它自己要消的那类假设上)**
③ 用 `command -v go` 探测,而 `command -v` **走 PATH**;④ 才修 PATH ⇒
**在 ④ 要修的那个环境里(PATH 为空),③ 的探测必然失败**,紧接着 ④ 把 PATH 装上、
后面的步骤**又能**找到 go —— **探测结论与实际可用性相反**。
实测(`env -i`):顺序翻转前 `go` 在 ③ 处找不到、④ 之后 `/usr/bin/go` 就在了。
已把 PATH 那段**挪到最前**,并在文件头写明顺序是**正确性而不是风格**。
(pi 自己也说了严重度:今天对 go 大概率无影响,因为 ① 已兜住 HOME、现代 go 会从 `$HOME/go`
自推缓存 —— 他把它当形状问题提,这个判断我认;一条探测所依赖的东西正是同文件后面要修的东西,
这正是本文件存在的理由。)

**2. `HOME=/root` 在非 root 调用者手里会把环境问题变成代码问题**
`redeploy-gateway.sh` 原先没有 EUID 断言(只有 `install.sh` 有),于是"非 root + 空 HOME"
会拿到 `HOME=/root`,接着 `go build`/`npm ci` 往 `/root/go`、`/root/.npm` 写 ⇒ **EACCES**,
而那串报错看起来是代码/工程问题 —— 正是本文件要消的东西。
已按身份分叉 + **验证可写**(判据落在"能不能写",不落在"路径长得像不像"),
兜底落到 `${TMPDIR:-/tmp}/agentmail-home-$(id -u)`;连一处可写的都找不到 ⇒ exit 2 + 人话。

★ **顺着他的思路又实测出第二个口子**:`HOME` **已给**但不可写时,上面只判"未设"就放行 ——
后果与空 HOME 完全相同,只是入参不同(`sudo -E`、从 root shell 继承、容器挂错)。
`mkdir -p` 对**已存在的不可写目录会返回成功**,所以必须单独判 `-w`。
实测 `setpriv --reuid=65534 env -i HOME=/root` ⇒ `touch $HOME/probe` 被拒。
已覆盖"已给但不可写",并**先说清再改判**(`[WARN] 调用者给的 HOME=… 不可写;改判到 …`),
不静默换目录 —— 静默换会让"东西写到哪去了"变成谜。
五种场景实测(全空 / 环境齐 / 非 root+空 / 非 root+不可写 HOME / root+可写):全部符合预期。

**3. "某条脚本忘了 source"没有信号** ⇒ 采纳
`AGENTMAIL_ENV_DEFAULTS` 只是被 export、值不进正常输出 ⇒ 谁把 `source` 删了,
输出与"环境本来就齐"**完全同形**(又是"看起来在兜、其实没兜")。
新增 `agentmail_env_report()`,三个脚本各自打一行(兜了哪些 / "(无 —— 调用者已提供全部)";
忘了 source 就没有这一行)。实测三个脚本在空环境下各自都打出来了 ——
这也把验收从"一条脚本"变成"三条各自可读"。

**4. 顺带补 `redeploy-gateway.sh` 的 root 断言**
它要往 `$PREFIX`(默认 /opt/agentmail)写,非 root 必然失败在写权限上,
而报错来自 `install`/`cp`、看起来像工程问题。用退出码 **2**(环境/权限),口径与 env-defaults 一致。
实测非 root 下:`[FAIL] 环境不足:本脚本要写 /opt/agentmail,需要 root。` 退出码 2。

验证:install.sh --check 空环境 exit 0、正常环境 exit 0;npm test exit 0;
prune 自检 22/22;drift 自检 35/0;check-shared-libs exit 0;四个脚本 bash -n 通过。
2026-09-14 21:01:19 +08:00

135 lines
7.3 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=""
# ④(**必须排在最前**,见文件头)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
# ① 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
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# }"
# 调用者打一行,让"兜了什么"**可见**(也是"忘了 source"的信号 —— 少了这行就不对)。
agentmail_env_report() {
if [ -n "$AGENTMAIL_ENV_DEFAULTS" ]; then
printf ' 环境兜底:%s\n' "$AGENTMAIL_ENV_DEFAULTS"
else
printf ' 环境兜底:(无 —— 调用者已提供全部)\n'
fi
}