From 3be824849d836733444b0dc57957c1df1f173ce7 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Mon, 14 Sep 2026 21:01:19 +0800 Subject: [PATCH] =?UTF-8?q?fix(deploy):=20env-defaults=20=E7=9A=84?= =?UTF-8?q?=E9=A1=BA=E5=BA=8F=E9=94=99=EF=BC=88=E2=91=A2=20=E7=9A=84?= =?UTF-8?q?=E6=8E=A2=E6=B5=8B=E4=BE=9D=E8=B5=96=20=E2=91=A3=20=E7=9A=84?= =?UTF-8?q?=E4=BA=A7=E7=89=A9=EF=BC=89+=20=E9=9D=9E=20root=20=E7=9A=84=20H?= =?UTF-8?q?OME=20=E9=99=B7=E9=98=B1=20+=20=E5=85=9C=E5=BA=95=E8=A1=8C?= =?UTF-8?q?=E5=8F=AF=E8=A7=81=20+=20=E8=A1=A5=20root=20=E6=96=AD=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 通过。 --- deploy/install.sh | 1 + deploy/lib/env-defaults.sh | 98 ++++++++++++++++++++++++++++++++------ deploy/redeploy-gateway.sh | 13 +++++ deploy/redeploy-plugin.sh | 1 + 4 files changed, 98 insertions(+), 15 deletions(-) diff --git a/deploy/install.sh b/deploy/install.sh index 443e8e9..4086e96 100755 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -14,6 +14,7 @@ REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # 理由与四次历史见该文件的头注释 —— pi 的原话是"否则第五次一定会来"。 # shellcheck source=./lib/env-defaults.sh . "$REPO/deploy/lib/env-defaults.sh" +agentmail_env_report # 与 `redeploy-gateway.sh` / `reset-demo.sh` 同源(pi 评审 2026-09-14 对出来的): # 那两处都写 `${AGENTMAIL_PREFIX:-/opt/agentmail}`,只有这里写死 —— # 谁设了那个变量,install 装到 A、redeploy 和 drift-check 看 B,三个工具各说各话。 diff --git a/deploy/lib/env-defaults.sh b/deploy/lib/env-defaults.sh index 72ac1b3..2639f53 100644 --- a/deploy/lib/env-defaults.sh +++ b/deploy/lib/env-defaults.sh @@ -24,17 +24,82 @@ # · 本文件只设**未设**的变量(`:-` 语义),绝不覆盖调用者已经给的值; # · 失败要**说清是环境问题**并按仓库约定用退出码 2(环境),不要让它冒充代码缺陷。 # -# 注意:本文件被 `source`,**不能带 `set -e`**(那会改调用者的语义)。 +# ⚠️ **顺序不是风格,是正确性**(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 跑部署时它通常就是 /root;显式给出以免"沙箱/最小环境下 HOME 为空"。 +# +# ★ **不能无脑给 `/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 - HOME=/root - export HOME - AGENTMAIL_ENV_DEFAULTS="${AGENTMAIL_ENV_DEFAULTS:-} HOME=$HOME" + 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 可能与真实挂载不符, @@ -42,25 +107,28 @@ fi if [ -z "${TMPDIR:-}" ]; then TMPDIR=/tmp export TMPDIR - AGENTMAIL_ENV_DEFAULTS="${AGENTMAIL_ENV_DEFAULTS:-} TMPDIR=$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" + AGENTMAIL_ENV_DEFAULTS="$AGENTMAIL_ENV_DEFAULTS GOPATH=$GOPATH" fi fi -# ④ 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 +export AGENTMAIL_ENV_DEFAULTS="${AGENTMAIL_ENV_DEFAULTS# }" -# 给调用者一句可打印的话:本次**兜了哪几个**变量(空则说明环境本来就齐)。 -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 +} diff --git a/deploy/redeploy-gateway.sh b/deploy/redeploy-gateway.sh index d84faac..04629e3 100755 --- a/deploy/redeploy-gateway.sh +++ b/deploy/redeploy-gateway.sh @@ -32,6 +32,19 @@ PREFIX="${AGENTMAIL_PREFIX:-/opt/agentmail}" # 环境自足:一处给全(理由与四次历史见该文件头注释) # shellcheck source=./lib/env-defaults.sh . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/env-defaults.sh" +agentmail_env_report + +# ★ root 断言(pi 评审 2026-09-14 指出本脚本缺它,只有 install.sh 有): +# 本脚本要往 $PREFIX(默认 /opt/agentmail,系统路径)写 —— 非 root 必然失败在写权限上。 +# 而失败时的报错来自 `install`/`cp`,看起来像**工程问题**。 +# 与 env-defaults.sh 里那条"别把环境问题报成代码问题"是同一条纪律: +# 与其让它晚一点、以晦涩的方式失败,不如在这里一行说清。 +# 用退出码 2(环境/权限),与 env-defaults.sh 的口径一致,不冒充判据失败(1)。 +[ "$(id -u)" = "0" ] || { + printf '\n [FAIL] 环境不足:本脚本要写 %s,需要 root。\n' "$PREFIX" >&2 + printf ' 药方:sudo bash deploy/redeploy-gateway.sh\n' >&2 + exit 2 +} TARGET="$PREFIX/agentmail-gateway" DB="$PREFIX/data/agentmail.db" SERVICE="agentmail-gateway" diff --git a/deploy/redeploy-plugin.sh b/deploy/redeploy-plugin.sh index 55de760..6415a45 100755 --- a/deploy/redeploy-plugin.sh +++ b/deploy/redeploy-plugin.sh @@ -50,6 +50,7 @@ REPO=${REPO:-/home/program/agentmail} # (`TMPDIR=${TMPDIR:-/tmp} npx tsc …`)—— 那正是"第四次"那种分散补丁的形状。 # shellcheck source=./lib/env-defaults.sh . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/env-defaults.sh" +agentmail_env_report GATEWAY_DB=${GATEWAY_DB:-/opt/agentmail/data/agentmail.db} DEST_ROOT=${DEST_ROOT:-/opt/agentmail/plugins} STAGE_ONLY=0