chore(deploy)★★: 固定端口监听扩展的失效防护 + SSE 投递审计脚本

## 一次真实事故(2026-10-03)

pi-web 会话里**所有**子 agent 都启动失败,报错

    Error: listen EADDRINUSE: address already in use 127.0.0.1:14010

这个扩展把端口写死,于是「端口被占」直接变成**全部启动失败**,
没有任何降级路径。这次提交把那条路径变成可测的。

- `check-listen-portability.mjs`(360 行):主判据
- `mutate-listen-portability.py`(124 行):变异体 —— 确认判据抓得住
- `verify-ext-parent-path.mjs` / `verify-ext-port-taken.mjs`:
  两条独立路径。**"端口已被占用"那一条之前从没真正跑完过** ——
  它原先被塞在一条 `node -e` 里,进程在「await 端口探测」那步被 30s
  超时掐断,而它恰恰是唯一会决定「子进程会不会被打死」的分支。

## scripts/sse-delivery-audit.sh(298 行)

从网关 access log 判定「SSE 那批邮件到底投没投出去」,**只读**。

为什么必须进版本库而不是 `.tmp/`:`.tmp/` 在 .gitignore 里,
写在 `.tmp/stress-test.sh` 的判据下一轮就没了 ——
而 `docs/DEBTS.json` 里 `stress-sse-delivered-but-silent-no-assertion`
那笔债会长期挂着。**判据和账本必须同寿命。**

## .gitignore

补 `/tmp/`:blob 阈值测试会生成 13MB 级 .bin
(`client/electron/test/tmp/att-threshold-test/`),可再生,不该进版本库
(本机实测 25MB)。
This commit is contained in:
2026-10-05 09:25:21 +08:00
parent 1f517eca4b
commit be9bfd3cd1
6 changed files with 944 additions and 0 deletions

View File

@ -0,0 +1,75 @@
/**
* 验证「端口已被占用」这条路径 —— 也就是**事故本身那一刻**的状态。
*
* 为什么单独一个文件:之前我把它塞在一条 `node -e` 里,进程在
* 「await 端口探测」那步被 30s 超时掐断(Command aborted),
* 于是这条路径**从来没真正跑完过**。而它恰恰是唯一会决定
* 「子进程会不会被打死」的分支。
*
* 两层断言:
* 1. 进程存活到结束(事故根因:不加 on("error") 时 Node 抛未捕获异常)
* 2. 该进程没有抢占已占用的端口(语义正确性:让给真正的占用者)
*/
import { createServer } from "node:http";
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const AGENT = "/root/.pi/agent";
const target = process.argv[2] ?? "pi-a2a";
const PORT = Number(process.argv[3] ?? 14779);
const srvKey = target === "pi-acp" ? "__piAcpServer" : "__piA2aServer";
// 先占住端口,模拟「父进程已经监听」
const blocker = createServer(() => {});
await new Promise((r) => blocker.listen(PORT, "127.0.0.1", r));
console.log(`SETUP: 端口 ${PORT} 已被本进程占用(模拟父进程)`);
process.env.PI_A2A_PORT = String(PORT);
process.env.PI_ACP_PORT = String(PORT);
delete process.env.PI_SUBAGENT_CHILD; // 走「非子进程但端口被占」这条分支
delete process.env.PI_SUBAGENT_PARENT_DEPTH;
const jiti = require(`${AGENT}/npm/node_modules/jiti/lib/jiti.mjs`).createJiti(import.meta.url, {
interopDefault: true,
fsCache: false,
moduleCache: false,
});
const factory = (await jiti.import(`${AGENT}/extensions/${target}/index.ts`)).default;
const tools = [];
factory({ registerTool: (t) => tools.push(t.name), on: () => {}, sendUserMessage: () => {} });
// 扩展的端口探测是异步的(300ms 超时),给它时间
await new Promise((r) => setTimeout(r, 1200));
const srv = globalThis[srvKey];
const problems = [];
if (srv && srv.listening) {
problems.push(`抢占了已占用端口 ${PORT}(语义错误)`);
}
if (srv && srv.address()) {
problems.push("仍报告了一个 bound address(应为空)");
}
if (tools.length === 0) {
problems.push("工具未注册 —— 扩展功能被降级掉了(出站工具必须仍可用)");
}
// 断言 1:占位者仍能应答(没被踢掉)
const alive = await new Promise((r) => {
const req = blocker.listening;
r(req);
});
if (!alive) problems.push("占用者不再监听");
console.log(`OBSERVED: ${target} 创建了 server=${srv !== undefined}, 工具=[${tools.join(",")}]`);
if (problems.length) {
console.log(`RESULT=FAIL ${problems.join("; ")}`);
blocker.close();
process.exit(1);
}
console.log(`RESULT=PASS 进程存活且未抢占已占用端口;出站工具仍注册`);
blocker.close();
process.exit(0);