Files
MailUI4Agents/plugins/zcode-mail-bridge/test/is-main.test.mjs
JianFeeeee d09ef395b4 refactor(zcode): 删掉本地 MCP 服务器与整套执行门禁 —— MCP 已内置网关
## 删了什么

**本地 MCP 服务器**(工具面已内置于网关 `POST /api/v1/mcp`,见 457d160):

    mcp/server.mjs          stdio JSON-RPC 入口
    lib/tools.mjs           11 个工具(手抄网关语义 —— 已抓到两次抄错)
    lib/mcp-rpc.mjs         手写协议层
    test/{tools,mcp-rpc,generic-mcp}.test.mjs

**执行门禁整条链**(用户裁定:直接移除):

    lib/action-tools.mjs    run_command / write_file
    lib/approval.mjs        授权判定
    lib/grants-file.mjs     「一直同意」跨进程持久化
    lib/hook-policy.mjs     档位判定
    hooks/permission.mjs    PermissionRequest 钩子
    hooks/hooks.json        钩子注册
    test/{action-tools,approval,grants-file,hook-policy}.test.mjs
    test/manual/{gate-e2e.py,permission-e2e.mjs,gate-e2e-evidence.json}

## 为什么执行门禁可以整条删,而不是留着

`run_command` / `write_file` 的门禁是**双进程审批**设计:MCP 进程问人、
ZCode 钩子进程等回答、中间靠落盘授权表对齐。三样都依赖**本地 MCP 进程**。
进程没了之后:

    没有任何代码装载 buildActionTools   ← 实测确认(只剩测试在测它)

也就是说它已经是死代码,而死代码 + 它的判据会让人误以为「这个平台有执行面」。
留着比删掉更危险。

本平台现在的姿态是**失败关闭**:平台自带 32 项危险工具被禁用,
AgentMail 侧不提供任何执行类工具 ⇒ 模型没有执行面。

## detectModeEnforcement 重写

它原本有三条依据,现在只剩一条还成立:

1. ~~平台 PermissionRequest 钩子~~ —— 目录整个删了。**留着「钩子是否注册」
   的判据只会说谎。**
2. ~~我们自己的门禁~~ —— 删了。
3. ✅ `--disallowed-tools` 禁用清单 —— 仍在,且现在是**唯一**那道。

报 `native` 的含义随之收窄为「该档位真的**没有执行面**」,而不是以前那个
「有人会来问」。降级路径(清单被清空 ⇒ advisory)仍有效,实测:

    正常配置  → native   | 平台自带危险工具已禁用 32 项…⇒ 模型无任何执行面
    清单清空  → advisory | 禁用清单自检未通过…禁用清单就是唯一那道

## 清单与 package.json

`.zcode-plugin/plugin.json` 去掉 `mcpServers` 与 `hooks`(两者的目标都已不存在)。
`package.json` 去掉 `main` 与 `verify`(已无本地入口)。

## 误删与自查

删 `test/permission-grants.test.mjs` 时**误删了一个仍在使用的共用库的测试**
—— `lib/permission-grants.js` 四方同源,dsh / opencode / pi 都还在用。
`deploy/check-shared-libs.sh` 立刻报「共用测试缺失」把它抓出来,已恢复。
若没有那道检查,这会是一个静默的覆盖损失。

## 验证

    node --test 'test/*.test.mjs'      293/293 绿(原 352,删掉 59 格死代码判据)
    deploy/check-shared-libs.sh         四方同源 rc=0
    detectModeEnforcement 实测           native / advisory 两条路径都对

## 后续

本目录现在只剩**邮件驱动**(SSE 订阅 → 起一轮 → 回信)与共用库。
若将来要在 zcode 侧恢复执行能力,需要重新设计门禁 —— 现有形状不能复用,
因为它的双进程模型随本地 MCP 进程一起消失了。
2026-10-02 14:14:06 +08:00

70 lines
2.8 KiB
JavaScript
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.

/**
* 入口判断的测试 —— 这个缺陷只在**部署形态**下出现,所以必须专门钉住。
*
* 实测经过:生产布局是 `current` 软链,入口用
* `import.meta.url === 'file://' + process.argv[1]` 判断是否直接执行 ——
* `import.meta.url` 是解析过软链的真实路径,argv 是软链路径,两者不等,
* 于是 `main()` 从不执行:**没有输出、没有报错、退出码 0**。
*
* 从仓库路径跑(无软链)完全正常,所以本地怎么试都发现不了。
*/
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtemp, mkdir, writeFile, symlink, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';
import { isMainModule } from '../lib/is-main.mjs';
test('同一个文件的真实路径 → 是入口', () => {
const url = pathToFileURL('/etc/hostname').href;
assert.equal(isMainModule(url, '/etc/hostname'), true);
});
test('★ 通过软链指向自己 → 仍是入口(生产布局就是软链)', async () => {
const dir = await mkdtemp(join(tmpdir(), 'zc-is-main-'));
try {
const real = join(dir, 'real.mjs');
const link = join(dir, 'current.mjs');
await writeFile(real, '// x\n', 'utf8');
await symlink(real, link);
const url = pathToFileURL(real).href;
assert.equal(
isMainModule(url, link),
true,
'软链路径必须被认成同一个文件 —— 否则快照部署下入口静默不执行'
);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
test('★ 目录软链(current → <时间戳>)下的完整路径同样成立', async () => {
// 生产的软链在**目录**这一层:/opt/.../<name>/current/src/index.mjs
const dir = await mkdtemp(join(tmpdir(), 'zc-is-main-d-'));
try {
await mkdir(join(dir, '20260101-000000', 'src'), { recursive: true });
const real = join(dir, "20260101-000000", "src", "index.mjs");
await writeFile(real, '// x\n', 'utf8');
await symlink('20260101-000000', join(dir, 'current'));
assert.equal(
isMainModule(pathToFileURL(real).href, join(dir, "current", "src", "index.mjs")),
true
);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
test('★ 反向对照:别的文件不是入口', () => {
assert.equal(isMainModule(pathToFileURL('/etc/hostname').href, '/etc/hosts'), false);
});
test('缺失的 argv 或文件不存在 → 保守判否(不误跑一遍)', () => {
assert.equal(isMainModule(pathToFileURL('/etc/hostname').href, ''), false);
assert.equal(isMainModule(pathToFileURL('/etc/hostname').href, undefined), false);
assert.equal(isMainModule(pathToFileURL('/etc/hostname').href, '/no/such/file/xyz'), false);
assert.equal(isMainModule('', '/etc/hostname'), false);
});