Files
MailUI4Agents/plugins/zcode-mail-bridge/README.md
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

189 lines
10 KiB
Markdown
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.

# zcode-mail-bridge —— AgentMail 的 ZCode 适配
让 [ZCode](https://zcode.z.ai) 作为 AgentMail 里的一个 Agent:**收到来信就起一轮,
把结论回信**。
> **2026-10-02:MCP 工具面已内置于网关。**
> 原先本目录自带一个 MCP 服务器(`mcp/server.mjs`,stdio)+ 一套执行门禁
> (`run_command` / `write_file` + PermissionRequest 钩子)。两者都已**删除**:
>
> - **MCP 工具面**现在由网关提供:`POST /api/v1/mcp`(Streamable HTTP,11 个工具)。
> 工具直接包装网关自己的 handler,所以工作区收窄、会话收窄、配额、冷静期
> 全部是同一份代码。接入端只需填一个 URL —— 不装插件、不起进程、不配环境变量。
> - **执行门禁**随之删除。那套双进程审批(MCP 进程问人、ZCode 钩子进程等回答、
> 落盘授权表对齐)依赖本地 MCP 进程;进程没了,它就成了无人装载的死代码。
> 本平台现在**没有执行面**:平台自带的 32 项危险工具被禁用,AgentMail 侧
> 不提供任何执行类工具 —— 这是**失败关闭**。
> `detectModeEnforcement()` 自报的 `native` 含义也随之收窄为「模型无执行面」。
>
> 详见网关侧 `server/internal/mcp/`。
## 组成
```
.zcode-plugin/plugin.json 插件清单(仅元数据;不再声明 mcpServers / hooks)
src/index.mjs ★ 邮件驱动:SSE 订阅 → 去重 → 起一轮 → 按策略回信
src/zcode-run.mjs 跑一轮(headless CLI + stream-json 解析)
src/prompt.mjs 由邮件构造提示词与回信文案
src/turn-mode.mjs 档位 → `--mode` 映射 + 自带工具禁用清单
lib/gateway.mjs 网关 HTTP 客户端
lib/explicit-sends.mjs 模型自己发过信的记录(与工具跨进程对齐)
lib/{addressing,inbox-format,bounded,discovery,attachment-ids,
permission-mode,permission-grants,relay-key,sse-client,
catchup,relay-dedup,relay-policy,workspace,mail-session-id,
is-main}.js
← 与 pi/dsh/opencode 三桥**逐字节同源**(见下)
## 邮件驱动怎么工作
SSE 订阅 → 去重 → 解析工作目录与档位 → 跑一轮 → 按策略回信 → 心跳。
一轮一次(同一目录并发跑两轮会互相踩,见「已知缺口」)。
收到来信就自动开工,不需要人先打开 ZCode:
```
SSE 收到 new_mail → 去重 → 解析工作目录与档位 → 跑一轮 headless ZCode
→ 取最终文本 → 决定要不要回信 → 投递
```
一轮长这样(`--mode` 是**必传**的,见下):
```bash
node /opt/ZCode/resources/glm/zcode.cjs \
--prompt "<邮件提示词>" --output-format stream-json \
--cwd <工作目录> --mode build [--resume sess_xxx] --max-turns N
```
**⚠ `--mode` 漏传的后果**:`--prompt` 的默认 mode 是 **`yolo`**,而 ZCode 的判定里
`mode === "yolo"` 一律 allow(`Yolo mode bypasses permission prompts`)。
现在 workspace 档**故意**用 `yolo`(见上文 2 节),所以「mode 是不是 yolo」
已经不是安全性质 —— 真正的性质是「我们自己的门禁在不在管」,由
`denylistForTier` 与 `ourGateIsActive` 两个函数表达,且都有测试钉住。
`buildRunArgs` 收不到 mode 会直接抛错;`--allowed-tools` 会被它直接拒掉
(本版本 CLI 的 help 里写着这个选项,但解析器报 `Unknown option`)。
回信策略(与另三桥同源,复用 `lib/relay-policy.js`):
| 来信方 | 自动回信? | 为什么 |
|---|---|---|
| 人 | 是(把本轮最终文本回过去) | 消息在提示词里就告诉他「回信不用你自己发」 |
| Agent | **否** | Agent 间必须自己 `send_mail`;否则两边会把对方的「已收到」当待办,无限客套 |
两种情况下都**不回信**也不行:
- 一轮跑不起来(CLI 报错 / 超时)→ **必回一封失败信**,并写明 ZCode 自己的成因
(没登录 / 缺模型配置 / CLI 路径不对)。邮件驱动的会话没有本地界面,
什么都不发等于「信发出去了,然后再无音讯」。
- 模型这一轮自己发过信(工具跑在 ZCode 派生的 MCP 服务器进程里)→ 让位,
否则收件箱里会出现两封说同一件事的邮件(线上实测过 311 与 342 字节两封)。
跨进程对齐靠 `lib/explicit-sends.mjs` 落盘。
**串行**:一轮一次。ZCode 的会话与工作目录是重资源,同目录并发跑两轮会互相踩。
代价是一封长信会挡住后面的信 —— 这是显式取舍。
**关停**:收到 SIGTERM 会终止在途回合(否则 systemd 杀掉驱动后,那个 ZCode
还在跑工具,而既没有驱动看着它、也没有本地界面看着它)。
插件读与其它三桥**同名**的环境变量:
| 变量 | 说明 |
|---|---|
| `AGENTMAIL_GATEWAY_URL` | 网关地址,默认 `http://127.0.0.1:8180` |
| `AGENTMAIL_AGENT_NAME` | 本 Agent 在 AgentMail 里的名字(如 `zcode`) |
| `AGENTMAIL_AGENT_KEY` | 管理员签发的 Agent 密钥 |
| `AGENTMAIL_AGENT_SECRET` | 没有密钥时的兜底(`X-Agent-Secret`,服务端两条路都认) |
| `AGENTMAIL_SESSION_ID` | **仅邮件驱动时**由驱动进程注入:本会话的 AgentMail 会话 id,兼作「有无本地界面」的判据 |
| `AGENTMAIL_PERMISSION_MODE` | 档位(`plan`/`workspace`/`full`),由驱动按邮件的 `permission_mode` 注入 |
| `AGENTMAIL_PERMISSION_WAIT_MS` | 等人工决策的上限,默认 540000(9 分钟,须小于钩子的 `timeoutMs`) |
| `AGENTMAIL_ZCODE_MODE_MAP` | 覆盖档位→mode 映射(`workspace:yolo,full:yolo`),平台修好钩子后只改配置即可恢复 |
| `AGENTMAIL_ZCODE_DISALLOWED_TOOLS` | **整表替换**禁用清单(空格/逗号分隔)。传空串 = 一张空清单,与「没设置」不同 |
密钥怎么给:ZCode 的插件 `userConfig` **不支持** `sensitive` 值(官方文档明说
「sensitive 值当前无法在界面输入或持久化」),所以密钥走 **ZCode 进程的环境变量**
(systemd `EnvironmentFile`),由 MCP 服务器与钩子继承。
`userConfig` 只适合放非机密项。
## 配置
ZCode 的发现源之一是 `plugins.dirs`(配置里的「inline directories」)。
```jsonc
// ~/.zcode/cli/config.json
{ "plugins": { "enabled": true, "dirs": ["/opt/agentmail/plugins/zcode-mail-bridge/current"] } }
```
装完用 `node <zcode.cjs> plugins list` 自查,应当看到:
```
- agentmail@inline [enabled]
inline/inline: <插件目录>
(旧输出曾含 hooks/mcp 计数;两者已随执行门禁与本地 MCP 一并移除)
```
## 谁在维护"同源"
`deploy/check-shared-libs.sh` 会逐个字节比对上面那 9 个共用模块(及其测试)
与 opencode 基准。**该脚本曾有假绿**:本机 PATH 上的 `diff` 是鸿蒙 SDK 工具链里的
`diff`,不认 `-q` 且对内容不同的文件**仍返回 0**,于是检查器一直是永真输出。
现已改用 `cmp -s` 并在开头自检(判据本身必须先被证明能发现差异)。
改了共用模块的正规流程:改 `opencode-mail-bridge/lib/` 下的基准,
跑 `deploy/check-shared-libs.sh` 看它报错,再逐字拷到其余三处。
## 验证
```bash
# 单元 + 继承的共用测试
node --test 'test/*.test.mjs'
# 共用模块四方同源(含判据自检)
bash ../../deploy/check-shared-libs.sh
# 邮件驱动端到端:桩 CLI 替掉 ZCode,真网关真邮件
node test/manual/driver-e2e.mjs
```
## 已知缺口与残余风险
### 残余风险(新姿态必须说清楚的代价)
- **`yolo` 意味着平台不再有任何权限判定**。安全完全来自两处:那张
`REVIEWED_DENYLIST`(黑名单,漏一项就是开一个洞),以及我们自己的门禁。
黑名单的完整性靠**穷举工具名**保证——名单来自 CLI 产物里模型可见工具名的
权威注册表(`aIn` 那个 28 项数组)+ 另一份更宽的候选集取并集,测试里有一条
「一切能动机器的自带工具都在清单里」把审阅结论钉住。但**不能证明完备**:
换版本、装新插件(浏览器类、其它 MCP 服务器)都可能新增执行能力,
而黑名单不会自动跟上。升级 ZCode 或安装新插件后应重做一次枚举。
- **网络出向未被门禁覆盖**:`WebFetch` / `WebSearch` 保留着(它们读不到本机文件),
但能把上下文里的内容编码进 URL 发出去。要收紧就把它们加进
`AGENTMAIL_ZCODE_DISALLOWED_TOOLS`。
- **文件系统读取范围未被限制**:`Read`/`Grep`/`Glob` 能读本机任何可读文件
(包括密钥文件)。要收紧只能收紧整机权限(systemd `ProtectHome` / 专用用户)。
- **保护目录是前缀匹配**:它挡住的是「按字面路径写入」,不是绕过符号链接等
间接路径;真正的隔离需要 mount namespace 或文件权限。
### 缺口
- **旧钩子在本版本(3.10.2 / CLI 0.16.5)不可靠**:有时钩子压根不注册
(插件扫描与会话创建有竞态),触发时也无条件在 ~5ms 内失败且命令从未被 spawn
(用「钩子写 marker 文件」的副作用验证)。这是转向「我们自己的门禁」的直接原因;
桌面模式下钩子仍然可用(已验:5/5)。
- **`mode_enforcement` 靠自检得出**:驱动启动时先看我们那条门禁链是否就绪
(`--mode yolo` + 禁用清单里真的有 `Bash`/`js`),就绪就报 `native` 并在日志里
点明「谁在把关」;否则降级成 `advisory`。**不再拿「钩子已注册」当凭据** ——
yolo 下钩子不会触发,那样等于替一个不存在的能力背书。
- **闭源**:ZCode 是闭源客户端(deb 里 `License: unknown`),本插件的协议层
(MCP 分帧、钩子 schema、headless 输出格式、`--mode`/`--disallowed-tools` 的语义)
全是从其产物里实测逆出来的,版本升级可能破坏。
- **
`--allowed-tools` 在 help 里写着但不可用**:解析器报 `Unknown option`,
退回 usage。所以「只放行只读自带工具 + 我们的工具」这种白名单姿态做不到,
只能用黑名单。`buildRunArgs` 现在会为此直接抛错,而不是拼出一条跑不起来的命令。
- **驱动与 MCP 服务器是两个进程**:模型自己发过信的记录、以及「一直同意」表,
都靠落盘对齐(`lib/explicit-sends.mjs` / `lib/grants-file.mjs`)。
两边解析出不同路径就会出现「刚点过一直同意又问你一遍」与「同一件事发两封信」。