fix(deploy): C 扩到全文件(133/0 闭合)+ ① 加 realpath 判据 + ②b 明说"恒等" + 环境自足收成一处

pi 这一封四个实质点,逐个实测后处理:

**C. 口径扩到全部文件**(他给的是算术,不是口味,我认):
原先按后缀取(`.conf/.service/.timer/.bak*`),我说的"零违规就不扩"是把口味当论证。
他把成本量化了:差集极小 ⇒ 多读几次文件(几十 KB),而收益是那个 `0` 从
**"有范围的 0"**(只对我划的圈成立)变成**"闭合的 0"**(对整棵 /etc/systemd 成立)。
他还补了一句我没想到的:这条判据只报**内容里含仓库路径**的文件,
所以含仓库串的 `.dpkg-old`/`~`/无后缀文件**恰恰都是真信号**(过期的旧真相),
不是噪声 —— 我先前"二进制会变成噪声"的担心本来就不成立。
**验收实测:比了 133 个文件(全部,不筛后缀)、命中 0。**
另按他要求把"零违规"这个前提写进注释,并说明"红/WARN 拆分"为什么推迟
(零违规时拆分是重构不是修 bug;出现第一个非白名单命中时再决定分档)。

**反例 1(②b 对 pi 是跑不到的分支)**:确认。pi 的依赖是全局包软链
(`-> /usr/lib/node_modules/@earendil-works/pi-coding-agent`),`cp -a` 保留软链
⇒ 两侧 realpath 到**同一个 inode**(实测 `statSync(a).ino === statSync(b).ino`)
⇒ 版本集合按构造相等 ⇒ **②b 对 pi 永远不会红**。这正是本文件自己列过的第三种形态
(断言在、区分力不在),比"没写判据"更坏因为它看起来是绿的。
已改:两侧 realpath 相同时**明说"恒等、区分力为零"**并指出它真正覆盖谁(有 vendored 树的宿主),
不再报"版本集合一致"这种让人误以为验过的措辞。自检加了这一条。

**反例 2(① 的 realpath 盲区)**:确认,形状真实且三条判据全都看不见 ——
①只 grep 内容(仓库那份 unit 文本里没有仓库字面量)、②比内容(live 就是 repo 那个 inode,
必然"一致")、④只查固定名单。已加 realpath 判据:被检文件 realpath 落在仓库里 ⇒ 红,
与内容无关。自检加**正反两面**(内容干净但指向仓库 ⇒ 红;指向仓库外 ⇒ 不许红,
否则这条判据恒红)。实测:本机 `/etc/systemd/system` 下 0 条指向仓库的软链
(即这个 0 现在才是闭合的)。

**反例 3(环境假设第四次 ⇒ 建议收成一处)**:采纳。四次的形态一模一样
(HOME ⇒ 又一次 HOME ⇒ TMPDIR ⇒ GOMODCACHE/GOPATH),每次"再加一个预检"只挡已知那一个。
新增 `deploy/lib/env-defaults.sh`:一处给全 HOME/TMPDIR/GOMODCACHE(GOPATH)/PATH,
只设**未设**的变量,注释里写明四次历史与"否则第五次一定会来";
三个部署脚本开头 source 它;**删掉**我上一轮加的那个分散 go 预检。
实测:在 `HOME`/`TMPDIR`/`GOPATH`/`GOMODCACHE` **全空**的环境里
`bash deploy/install.sh --check` **exit 0**(go vet + go test 自己站起来),
兜住的变量会在 `AGENTMAIL_ENV_DEFAULTS` 里说明。

docs 补两条纪律:13「锚点必须一一对应 —— 连'文件名'都会骗你」(E 的探针教训)、
14「退出码也有量纲」(--self-check 的退出码不是自检的结论)。

验证:npm test exit 0;check-shared-libs exit 0;drift --self-check **35/0**;
prune 干跑 exit 0;install.sh --check 空环境 exit 0。
This commit is contained in:
2026-09-14 20:42:54 +08:00
parent d47035aaff
commit 7eec311756
6 changed files with 224 additions and 31 deletions

View File

@ -728,27 +728,55 @@ export function checkLayout(inject = {}) {
// 那句话只对我自己划的那个圈成立 —— **判据的边界没说出口,就等于报了个假的 0**。
// 留着 .bak 的代价也不是零:它们是"过期的旧真相",`grep` 到它的人会以为改动没生效。
const offenders = [];
// ★ **realpath 判据**(pi 反例 2026-09-14):软链指向仓库时,内容判据与 ② 都看不见 ——
// ① 只 grep 内容里有没有仓库字面量,而仓库那份 unit 的文本里**没有**那个字面量;
// ② 比 repo↔live 内容,而 live **就是** repo 那个文件(同一 inode)⇒ 必然"一致";
// ④ 只查名单里那几个固定 unit 名。
// 形状:`/etc/systemd/system/x.service -> /home/program/agentmail/deploy/systemd/x.service`
// —— 而"单元指向仓库"正是 ② 存在的理由(谁跑一次 install.sh 就把部署退回源码目录)。
// 所以这里按**真实路径**判,与内容无关。
const repoLinks = [];
const lstatForLinks = inject.lstat ?? lstatSync;
const realpathForLinks = inject.realpath ?? realpathSync;
let scanned = 0;
// ⚠️ **已知的圈**:这里按后缀取(`.conf/.service/.timer`,含 `.bak*` 变体)。
// pi 评审 2026-09-14 建议改成"不划圈"——`/etc/systemd/system` 下每个普通文件都读一遍
// 再 grep,理由是**白名单永远会漏一个**(`.dpkg-old`/`.rpmsave`/`~`/`.disabled`/…)。
let scannedLinks = 0;
// ★ **不划圈**:每个普通文件都读一遍再 grep(pi 评审 2026-09-14,我原先按后缀取)。
//
// 取舍(我做了、理由写在这里):**暂不改**,因为实测这一格现在是**零违规**:
// 该目录 133 个文件里,**没有任何一个**包含仓库路径 —— 包括现存的 4 个 `.bak`
// (`dsh-lan.service.bak-20260903-081410`、`pi-bridge.service.bak-13010-20260814`
// 等,全干净)。`pi` 说的 `zcode.service.bak-20260912-145744` 已经不在了
// (他自己读时也是 ENOENT),所以我引他那句时依据的文件**已消失**。
// 既然零违规,扩到"全文件"只增加噪声(二进制/dpkg 数据库类文件),换不到真信号;
// 因此改为**把覆盖面写进 note**(`0` 必须带上"它能被证伪的范围"),
// 并把这条决定留在注释里 —— 等真出现一个非白名单后缀的违规再改,那时我们就有实例了。
// 我原先把这条推迟了,理由是"实测零违规 ⇒ 扩口径只增噪声"。pi 用**算术**驳回了口味问题:
// 总文件 133(递归口径 129/129?见 note 实数)/ 现行后缀口径覆盖绝大多数 ⇒ 差集极小,
// 即"读全部"的噪声成本是**几次 `readFile`**(几十 KB),而收益是那个 `0` 从
// "**有范围的** 0"(只对我划的圈成立)变成"**闭合的** 0"(对整棵 /etc/systemd 成立)。
// 他这条论证我认:同一条"0 必须写明可证伪范围"的规矩,**闭合范围是同样成本、更强结论**。
// 而且他补了一句关键的:这条判据只报**内容里含仓库路径**的文件,
// 含仓库路径的 `.dpkg-old`/`~`/无后缀文件**恰恰都是真信号**(过期的旧真相)——
// 不是噪声。所以我先前"二进制会变成噪声"的担心本来就不成立(不含仓库串的不会被报)。
//
// 验收(他自己给的):扩完应当报"比了全部 N 个、命中 0 个";若命中里有文件名后缀异常者,
// 那就说明原先漏掉的正是真信号 —— 两个结果都赢。
// 实测验收:**比了 133 个文件(全部,不筛后缀)、命中 0** —— 即现在的 `0` 是**闭合的**。
//
// ★ **"零违规"这个前提必须写明**(pi 要求),也一并说明"红/WARN 拆分"为什么没做:
// 今天实测 0 违规 ⇒ 把"生效 unit 引用仓库"(红)与"遗留备份引用仓库"(WARN)拆成两档,
// 在当前是**重构、不是修 bug**,所以推迟;
// **出现第一个非白名单/备份类命中时再决定怎么分档**(那时我们才有实例,
// 而不是凭想象设计)。
const walk = dir => {
let entries = [];
try { entries = readdir(dir, { withFileTypes: true }); } catch { return; }
for (const e of entries) {
const full = `${dir}/${e.name}`;
if (e.isDirectory()) walk(full);
else if (/\.(conf|service|timer)(\.bak.*)?$/.test(e.name)) {
else {
// 不划圈:任何普通文件都读(见上面的论证)。软链在下面单独按 realpath 判。
scanned++;
// 软链另算:上面只 grep 了**内容**,跟随软链的单元必须按目标位置判。
try {
if (lstatForLinks(full).isSymbolicLink()) {
scannedLinks++;
const real = realpathForLinks(full);
if (real.startsWith(`${REPO}/`)) repoLinks.push(`${full} → ${real}`);
}
} catch { /* 悬空软链:读不到目标,交给 ② 的 walk 报 */ }
let text = '';
try { text = String(readFile(full, 'utf8')); } catch { continue; }
if (text.includes(REPO)) offenders.push(full);
@ -758,12 +786,14 @@ export function checkLayout(inject = {}) {
walk(SYS);
// 覆盖面写进 note:`0` 只有在"它能被证伪的范围"写明之后才是结论
// (这正是这条判据当初报"0 个文件"时缺的那句话)。
// 现在范围是**闭合**的(全部文件),不再只是"对我划的那个圈成立"。
const refOk = offenders.length === 0 && repoLinks.length === 0;
push(
'没有任何 unit/drop-in/.bak 引用源码目录',
offenders.length === 0,
offenders.length === 0
? `比了 ${scanned} 个 .conf/.service/.timer/.bak* 文件(覆盖面:按后缀取,见下)`
: offenders.join(' ')
'没有任何 unit/drop-in/.bak 引用源码目录(含软链指向仓库)',
refOk,
refOk
? `比了 ${scanned} 个文件(**全部**,不筛后缀)、命中 0;其中软链 ${scannedLinks} 个另按 realpath 判目标`
: [...offenders, ...repoLinks].join(' ')
);
// ② 已安装单元与仓库副本一致(仓库是唯一真相)—— **两个方向都判**
@ -892,7 +922,28 @@ export function checkLayout(inject = {}) {
else if (sa.sig !== sb.sig) {
depOk = false;
depNote = `依赖树不一致:仓库 ${sa.n} 个包 vs 已部署 ${sb.n} 个包(重新部署即可对齐)`;
} else depNote = `${sa.n} 个包,版本集合一致`;
} else {
// ★ 两侧**指向同一个文件**时,这条判据是**恒等**的 —— 必须说出来,不许报"一致"。
//
// pi 反例 2026-09-14:pi 的依赖是**全局包的符号链接**
// (`node_modules/@earendil-works/pi-coding-agent -> /usr/lib/node_modules/…`),
// 而 `redeploy-plugin.sh` 用 `cp -a`(保留软链)⇒ 快照里那个还是同一个软链
// ⇒ `findDepLock` 两侧 realpath 到**同一个 inode**,版本集合**按构造**就相等。
// 实测确认:两侧 realpath 都是 `/usr/lib/node_modules/…`、inode 相同。
// 也就是说它**永远不会因为"pi 的依赖变了"而红** —— 这是本文件自己列过的第三种形态
// (**跑不到的分支**:断言在、区分力不在),比"没写判据"更坏,
// 因为它看起来是绿的。真实风险(全局 SDK 被换掉)它同样看不见:那时两侧一起变。
// 所以:绿,但 note 明说"恒等",并指出它真正覆盖的是谁。
let same = false;
try {
const ra = (inject.realpath ?? realpathSync)(a);
const rb = (inject.realpath ?? realpathSync)(b);
same = ra === rb;
} catch { /* 读不到就不敢断言恒等 */ }
depNote = same
? `${sa.n} 个包,**两侧是同一个文件(${'(全局包的符号链接)'})⇒ 本判据对它恒等、区分力为零**;真正覆盖的是有 vendored 依赖树的宿主(opencode/dsh)`
: `${sa.n} 个包,版本集合一致`;
}
} else if (a || b) {
depOk = false;
depNote = `一侧有依赖树、另一侧没有(仓库 ${a ? '有' : '无'} / 已部署 ${b ? '有' : '无'})`;
@ -1040,6 +1091,36 @@ export function layoutSelfCheck() {
'/opt/agentmail/bin/service-failure-notify.mjs': 'x',
'/opt/agentmail/agentmail-gateway': 'fake-elf'
}));
// ★ ① 的**软链分支**:单元内容是干净的(不含仓库字面量),但软链**指向**仓库 ⇒ 必须红。
//
// 这条形状三条判据原先全都看不见(pi 反例):① 只 grep 内容、② 比的内容相同
// (live 就是 repo 那个 inode)、④ 只查固定名单。所以样本要故意让内容**干净**,
// 逼判据只能靠 realpath 抓到它 —— 否则这条样本会因为内容命中而"绿得毫无意义"。
const badLink = checkLayout({
...fake({
'/etc/systemd/system': [{ name: 'linked.service', isDirectory: () => false }],
// 内容里**没有**仓库路径:这正是真实软链的形状(仓库那份 unit 的文本)
'/etc/systemd/system/linked.service': 'ExecStart=/opt/agentmail/agentmail-gateway',
'/repo/systemd': [],
'/opt/agentmail/bin/service-failure-notify.mjs': 'x',
'/opt/agentmail/agentmail-gateway': 'fake-elf'
}),
lstat: p => ({ isSymbolicLink: () => p === '/etc/systemd/system/linked.service' }),
realpath: () => '/home/program/agentmail/deploy/systemd/linked.service'
});
// 反面对照:同一个形状但目标**不在**仓库里 ⇒ 不许报(否则这条判据会变成恒红)。
const goodLink = checkLayout({
...fake({
'/etc/systemd/system': [{ name: 'linked.service', isDirectory: () => false }],
'/etc/systemd/system/linked.service': 'ExecStart=/opt/agentmail/agentmail-gateway',
'/repo/systemd': [],
'/opt/agentmail/bin/service-failure-notify.mjs': 'x',
'/opt/agentmail/agentmail-gateway': 'fake-elf'
}),
lstat: p => ({ isSymbolicLink: () => p === '/etc/systemd/system/linked.service' }),
realpath: () => '/usr/lib/systemd/system/somewhere-else.service'
});
// ① 的 .bak 分支:旧备份单元里躺着仓库路径,也必须报出来(原先它被过滤掉了)。
const badBak = checkLayout(fake({
'/etc/systemd/system': [{ name: 'z.service.bak-20260101-000000', isDirectory: () => false }],
@ -1097,6 +1178,30 @@ export function layoutSelfCheck() {
JSON.stringify({ packages: Object.fromEntries(packages.map(p => [p, { version: p.split('@').pop() }])) }));
return root;
};
// ★ 两侧指向**同一个文件**时必须明说"恒等",不许报"一致"(pi 反例:pi 的依赖是全局包软链)。
const depSameFile = (() => {
let a = null;
try {
a = depFixture(['a@1.0.0']);
// 两侧都指向同一个真文件(经由同一路径),realpath 相等
const out = byName(
checkLayout({
...fake({ '/etc/systemd/system': [], '/repo/systemd': [] }),
repoPluginDir: a,
livePluginDir: a,
realpath: p => p,
readdir: (dir, opts) => (dir.startsWith(a) ? readdirSync(dir, opts) : []),
readFile: (p, enc) => (p.startsWith(a) ? readFileSync(p, enc) : (() => { throw new Error('ENOENT'); })()),
exists: p => p.startsWith(a)
}),
'已部署依赖树'
);
return out;
} finally {
if (a) rmSync(a, { recursive: true, force: true });
}
})();
const depSample = (repoPkgs, livePkgs) => {
let a = null; let b = null;
try {
@ -1127,10 +1232,17 @@ export function layoutSelfCheck() {
return [
{ name: '标准目录:引用源码目录的样本必须判红', ok: unitRefCheck(bad)?.ok === false },
// ★ 软链分支的正反两面都要真:内容干净但指向仓库 ⇒ 红;指向仓库外 ⇒ 绿。
{ name: '★单元是软链且指向仓库 ⇒ 必须红(内容判据看不见这个形状)',
ok: unitRefCheck(badLink)?.ok === false && /→/.test(unitRefCheck(badLink)?.note ?? '') },
{ name: '★软链指向仓库外 ⇒ 不许红(否则这条判据恒红)',
ok: unitRefCheck(goodLink)?.ok === true },
// 依赖树:一致必须绿、变了必须红、一侧没有必须红 —— 三面都钉。
{ name: '★依赖树一致 ⇒ 绿,且说出比了几个包', ok: sameDeps?.ok === true && /2 个包/.test(sameDeps?.note ?? '') },
{ name: '★依赖树版本变了 ⇒ 必须红', ok: diffDeps?.ok === false && /不一致/.test(diffDeps?.note ?? '') },
{ name: '★一侧没有依赖树 ⇒ 必须红(不许当"未比"放过)', ok: missingDeps?.ok === false },
{ name: '★两侧是同一个文件 ⇒ 明说"恒等/区分力为零",不许报"一致"',
ok: depSameFile?.ok === true && /恒等/.test(depSameFile?.note ?? '') },
// ③ 从"只判在不在"改成"比内容"之后,必须证明它真能发现内容不同(否则又是一条假绿)。
// 探针:把仓库那份读成别的内容 ⇒ ③ 必须红。
{

View File

@ -9,6 +9,11 @@
set -euo pipefail
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# 环境自足:一处给全(HOME/TMPDIR/GOMODCACHE/PATH),不在每条命令旁各写预检。
# 理由与四次历史见该文件的头注释 —— pi 的原话是"否则第五次一定会来"。
# shellcheck source=./lib/env-defaults.sh
. "$REPO/deploy/lib/env-defaults.sh"
# 与 `redeploy-gateway.sh` / `reset-demo.sh` 同源(pi 评审 2026-09-14 对出来的):
# 那两处都写 `${AGENTMAIL_PREFIX:-/opt/agentmail}`,只有这里写死 ——
# 谁设了那个变量,install 装到 A、redeploy 和 drift-check 看 B,三个工具各说各话。
@ -188,18 +193,8 @@ if [[ $CHECK_ONLY -eq 1 ]]; then
# 而"第一个拿到 root 的人第一次跑门禁"正是 `--check` 要解决的场景。
# 它们不写系统目录(只写 go 缓存),所以放进这个相位没有副作用。
echo "==> 校验 Gateway 源码(go vet + go test)"
# ★ 先判"go 到底跑不跑得动",把**环境问题**与"代码不过"分开。
# 实测(2026-09-14,我在沙箱里跑这一步时):没有 HOME/GOPATH 的环境下
# `go vet` 直接报 `go: module cache not found: neither GOMODCACHE nor GOPATH is set`,
# 于是这句话会说成"go vet / go test 不过 —— 先修好再安装" —— **把环境问题报成代码问题**,
# 而那正好诱导人去 sudo 建目录(GOPATH 落到 `${GOPATH:-$HOME/go}/pkg/mod`)。
# `go env GOMODCACHE` 在没有这两者时返回**空串**,一个既便宜又精确的信号。
GOMODCACHE_DIR="$(go env GOMODCACHE 2>/dev/null || true)"
if [ -z "$GOMODCACHE_DIR" ]; then
echo " [FAIL] 环境不足:\`go env GOMODCACHE\` 是空的(既没有 GOMODCACHE 也没有 GOPATH,通常是 HOME 没设)" >&2
echo " 这是**环境问题**,不是代码问题。药方:带上有 HOME 的环境重跑(如 sudo 或 HOME=/root)" >&2
exit 2
fi
# 环境不再在这里逐个预检 —— 开头 source 的 env-defaults.sh 已经保证
# HOME/TMPDIR/GOMODCACHE/PATH 齐备(那道预检是"第四次"的补丁,已被一处取代)。
if ! ( cd "$REPO/server" && go vet ./... && go test ./... ); then
echo " [FAIL] go vet / go test 不过 —— 先修好再安装" >&2
exit 1

View File

@ -0,0 +1,66 @@
#!/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(环境),不要让它冒充代码缺陷。
#
# 注意:本文件被 `source`,**不能带 `set -e`**(那会改调用者的语义)。
# 调用者自己已经有 `set -uo pipefail`(`install.sh` 另有 `-e`),这里不重复声明。
# shellcheck shell=bash
# ① HOME:`go`/`npm`/`npx` 都会用它推默认缓存与配置位置。
# 以 root 跑部署时它通常就是 /root;显式给出以免"沙箱/最小环境下 HOME 为空"。
if [ -z "${HOME:-}" ]; then
HOME=/root
export HOME
AGENTMAIL_ENV_DEFAULTS="${AGENTMAIL_ENV_DEFAULTS:-} HOME=$HOME"
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)。
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
# ④ 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:-}"

View File

@ -28,6 +28,10 @@ set -uo pipefail
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PREFIX="${AGENTMAIL_PREFIX:-/opt/agentmail}"
# 环境自足:一处给全(理由与四次历史见该文件头注释)
# shellcheck source=./lib/env-defaults.sh
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/env-defaults.sh"
TARGET="$PREFIX/agentmail-gateway"
DB="$PREFIX/data/agentmail.db"
SERVICE="agentmail-gateway"

View File

@ -45,6 +45,11 @@
set -uo pipefail
REPO=${REPO:-/home/program/agentmail}
# 环境自足:一处给全(HOME/TMPDIR/GOMODCACHE/PATH)。原先这里只有一行内联兜底
# (`TMPDIR=${TMPDIR:-/tmp} npx tsc …`)—— 那正是"第四次"那种分散补丁的形状。
# shellcheck source=./lib/env-defaults.sh
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/env-defaults.sh"
GATEWAY_DB=${GATEWAY_DB:-/opt/agentmail/data/agentmail.db}
DEST_ROOT=${DEST_ROOT:-/opt/agentmail/plugins}
STAGE_ONLY=0