refactor(plugins): 删掉「摆了一套权限规则却没人调用、且形状没人能消费」的死代码

# 问题

`lib/permission-mode.js` 里的 `opencodePermissions(mode)` 实现完整、注释详实
(含 6 条实测结论)、还有 9 条测试把行为钉住;opencode 的 `index.js` 第 45 行
确实 import 了它 —— 然后**全文件再没有第二次出现**。

静态审计要花力气才能发现它是死的,而读代码的人会理所当然地以为
「opencode 的 plan 档由这套规则拦着」。实际 opencode 是 advisory。

比 9.17(给了按钮不实现)更深一层:那只是没实现,这个是**看起来像实现**。

# 它不只是没接上,而是接不上

查 1.18.29 的 SDK 类型定义,三条都排除:

  SessionCreateData.body 只有 { parentID, title };query 只有 { directory }
      → 会话级根本没有 permission,也没有 agent
  permission 只存在于 Config / AgentConfig,且是 map 形状
      → { edit, bash, webfetch, doom_loop, external_directory } → ask|allow|deny
  Permission 事件(permission.updated 的 properties)没有 action 字段
      → { id, type, pattern?, sessionID, messageID, callID?, title, metadata, time }

而函数返回的是 `{permission, action, pattern}[]` —— **与三者都不匹配**,
并且 deny 了 `task`(本版本 permission 的合法键里没有 task)。

所以不是「加一行调用就生效」,而是**输出没有任何消费者**。

# 为什么 opencode 的 per-session 强制做不到(如实说明)

Config / AgentConfig 是配置文件级(全局或项目级)。邮件桥若靠改配置给某条会话
加 plan 限制,会连带锁住这个人**其他所有**会话的同一工具 —— 一条 plan 档的邮件
把人身兼的其他工作一起禁掉,不可接受。

opencode 目前唯一的拦截路径是「它自己先问 → 桥转发 → 服务端按档位 409 →
桥当场 block」,**前提是它的配置恰好是 ask**;若配置直接 allow,桥连
permission.updated 都看不到。这就是它只能是 advisory 的原因,不是缺工作量。

# 改动

- 删除 `opencodePermissions` 及其 doc 注释、`OpencodePermissionRule` 接口声明
  (三份共用库同时改,改后 md5 仍逐字节相同)
- 删除 opencode/index.js 里那行未使用的 import
- 删除 9 条针对该函数的断言(三桥各 9 条)
- **6 条实测结论没有丢** —— 搬进 `docs/PLUGIN-CONTRACT.md` 新增的 §9.18,
  连同上面那三条类型证据与「为什么 per-session 做不到」

删掉而非保留,是因为留下的就是陷阱:函数存在、注释写着「实测过」、
测试还全绿,唯一缺的是调用点 —— 下一个人会以为档位在这里被强制。

# 验证

- `deploy/check-shared-libs.sh` → 共用模块三方同源
- 三桥 `npm test` 全绿:pi 400 / dsh 360 / opencode 311,0 失败
  (删除前 pi 409 / opencode 320,各 −9 即被删断言;dsh 另有并发提交新增测试,
  净 −9 后为 360)
- `node --check` 四个改动文件全过;ESM 动态 import 该 lib 成功,
  导出里已无 `opencodePermissions`,index.js 只引用仍存在的符号
- 本改动不影响运行时行为(删的是一个从未执行的函数与一个未使用的 import)
This commit is contained in:
2026-09-11 23:59:24 +08:00
parent 4e32dd3145
commit a60ab66a40
9 changed files with 77 additions and 416 deletions

View File

@ -1741,6 +1741,79 @@ permission_requests.result 全是「同意」—— 一次「一直同意」都
---
### 9.18 摆一套权限规则,却从不调用,且形状根本没人能消费
`9.17`(给了按钮不实现)更深一层的错:**代码看起来像强制力的实现**。
`lib/permission-mode.js` 里曾有 `opencodePermissions(mode)`,实现完整、注释详实
(含 6 条实测结论)、还有 9 条测试把它的行为钉住。opencode 的 `index.js`
第 45 行确实 import 了它 —— 然后**全文件再也没有第二次出现**。
静态审计要花力气才能发现它是死的,而读代码的人会理所当然地以为
「opencode 的 plan 档由这套规则拦着」。实际不是opencode 是 `advisory`
#### 它不只是没接上,而是**接不上**
查 1.18.29 的 SDK 类型定义,三条都排除:
```ts
// ① 会话级参数里没有 permission也没有 agent只有这两个字段
export type SessionCreateData = {
body?: { parentID?: string; title?: string };
query?: { directory?: string };
};
// ② permission 只存在于 Config / AgentConfig而且是 map 形状
export type Config = { permission?: {
edit?: "ask"|"allow"|"deny"; bash?: ;
webfetch?: ; doom_loop?: ; external_directory?: ;
}; };
// ③ 权限事件也没有 action 字段
export type Permission = {
id, type, pattern?, sessionID, messageID, callID?, title, metadata, time
};
```
而那个函数返回的是 `{permission, action, pattern}[]` —— **与三者都不匹配**
并且 deny 了 `task`(本版本 `permission` 的合法键里**没有 task**)。
所以这不是「加一行调用就能生效」,而是**输出没有任何消费者**。
#### 为什么 per-session 强制在 opencode 上做不到
`Config` / `AgentConfig` 是**配置文件级**(全局或项目级)。邮件桥若靠改配置来
给某条会话加 plan 限制,会连带锁住这个人**其他所有**会话的同一工具 ——
一条 plan 档的邮件把人身兼的其他工作一起禁掉,不可接受。
opencode 目前的拦截路径只能是「它自己先问 → 桥转发 → 服务端按档位 409 →
桥当场 block」**前提是它的配置恰好是 `ask`**;若配置直接 `allow`
桥连 `permission.updated` 事件都看不到。这就是它只能是 `advisory` 的原因。
#### 处理:删掉函数,知识搬进本节
死代码留着就是陷阱。函数与 9 条测试已删除6 条实测结论保留在这里:
1. **`findLast` 胜出** —— 规则数组里 deny 必须排在 allow **之前**
反了的话最后匹配到 deny连允许的路径也被拒。
2. **pattern 匹配 worktree 相对路径** —— 写 `/tmp/**` 这种绝对 pattern 永远匹配不上
`/tmp/x` 相对 `/home/program/agentmail``../../../tmp/x`)。
3. **write / edit / patch 共用 `edit` 一个权限名**`if(A==="write"||A==="edit"||A==="patch"){G.edit=I}`)。
4. **全 deny 会让工具从模型清单里消失**模型自述「I don't have a bash tool available
in this session」部分 deny 则工具保留、越界调用才报错。
5. **`task`(子代理)能绕过父会话权限** —— 实测中模型发现自己没 write
主动 `task` 委派给一个带 write 的子代理去写成了。
6. **`bash` 能绕过 `edit` 的路径限制** —— 模型用 shell 重定向写成了本该被 deny 的文件。
另注opencode 原生有 `plan_enter` / `plan_exit` 权限项,与我们的 plan 档
**撞名但语义不同**(那是它自己的计划模式开关),不要碰它们。
> 自查:一个 `export function` 写完之后,除了它的测试,还有谁调用它?
> 只有 import 行、没有调用点时,先查**它的输出形状在目标平台里到底有没有消费者** ——
> 「形状没人认」比「忘了接线」更常见,且更隐蔽。
---
## 附:文档关系
| 文档 | 内容 |