- 任务感知 LLM 审查(UserPromptSubmit 捕获任务 + PostToolUse 组合风险) - 四层流水线:工具白名单 / cwd 内编辑快速路径 / 命令白名单 / 灾难命令黑名单确定性拦截 - 防提示注入:授权只认键盘捕获段,伪造授权直接拒绝 - onUncertain 三值可配(deny/prompt/allow)+ ZCode 原生弹窗升级(已实测) - approve 一次性放行逃生舱(精确匹配、用后即焚、审计留痕) - 常驻 daemon:TLS 连接热复用、决策缓存、配置热重载、内联降级兜底 - 一键 install/uninstall/status、extraBody 请求体注入、29 项回归测试
216 lines
13 KiB
Markdown
216 lines
13 KiB
Markdown
# ZCode Auto 审批员(gatekeeper)
|
||
|
||
对标 **Claude Code auto mode** 的 ZCode 外置实现:由一个独立的小模型在 ZCode 每次工具调用执行前,对照"当前任务"审查是否越权——
|
||
|
||
- 任务内的操作**自动放行**,不打断你;
|
||
- 越权/危险的**拒绝并说明理由**,理由回传给主模型,它会自行调整方案;
|
||
- 审查器自身故障时 **fail-closed**(拒绝执行,与 Claude Code 的兜底语义一致)。
|
||
|
||
审查分类、决策清单、`shouldBlock` 判定 schema 与 fail-closed 语义均对照 Claude Code auto mode 的实际行为设计。
|
||
|
||
## 功能特性
|
||
|
||
- 任务感知:审查员能看到你最近输入的任务,据此判断"是否越权",而不是死板的关键词匹配
|
||
- 全工具覆盖:shell 命令、文件写入、MCP 工具都在审查范围内
|
||
- 分层流水线:只读操作零延迟直通,灰区才调用审查模型(实测 ~0.7–1.5s)
|
||
- 灾难命令黑名单:删盘/格式化/递归删除根目录等**不经任何模型、确定性拦截**
|
||
- 弹窗分级可配:模型"拿不准"时可以选择弹窗问你、硬拒或放行(`onUncertain`)
|
||
- 逃生舱:被拒的命令支持一次性批准(精确匹配、用后即焚、审计留痕)
|
||
- 防注入:模型伪造"用户已同意"等授权文字会被识别为注入并直接拒绝
|
||
- 常驻 daemon:审查连接热复用、决策缓存、配置热重载
|
||
- 审计日志:每次决策(放行/拒绝/来源/耗时)全部落盘可查
|
||
|
||
## 环境要求
|
||
|
||
- ZCode 桌面版(hooks 在会话启动时加载,改配置后需重启会话)
|
||
- Node.js ≥ 18(自带 fetch,零第三方依赖)
|
||
- 一个 OpenAI 兼容的审查端点(任意提供 chat/completions 的服务)
|
||
|
||
## 快速开始
|
||
|
||
```bash
|
||
git clone <本仓库> && cd <仓库目录>
|
||
node gatekeeper/gatekeeper.mjs install # 一键安装
|
||
```
|
||
|
||
安装器做了三件事:把运行文件复制到 `~/.zcode/gatekeeper/`;在用户级配置 `~/.zcode/cli/config.json` 注册 4 个 hooks(只增删本工具自己的条目,不动你已有的其他 hooks,改动前自动备份);打印后续步骤。
|
||
|
||
然后:
|
||
|
||
1. 编辑 `~/.zcode/gatekeeper/config.json`,填入你的审查端点(字段说明见 [配置参考](#configjson-配置参考),模板见 `gatekeeper/config.example.json`);
|
||
2. **重启 ZCode 会话**(hooks 在会话启动时快照);
|
||
3. 跑一条命令试试:`node gatekeeper.mjs status` 查看状态,`tail -f gatekeeper/state/audit.jsonl` 观察每次决策。
|
||
|
||
## 工作原理
|
||
|
||
注册 4 个 ZCode hooks(用户级 `~/.zcode/cli/config.json`):
|
||
|
||
| 事件 | 模式 | 作用 |
|
||
|---|---|---|
|
||
| `UserPromptSubmit` | `capture-prompt` | 捕获用户输入的任务原文(最近 5 条),按会话存盘——这是"任务感知"的关键 |
|
||
| `PostToolUse` | `capture-tool` | 记录最近 10 次工具调用,供审查器评估"连续安全操作的组合风险" |
|
||
| `PreToolUse` | `review --layer=hard` | 硬安全网(每次工具调用都过):只读工具白名单直放 + 灾难命令黑名单确定性拦截(不经任何模型) |
|
||
| `PermissionRequest` | `review --layer=full` | 完整审查(ZCode 需要权限决定时):白名单/工作区内编辑快速放行 → 灰区交给审查模型 |
|
||
|
||
完整层的判定流水线:
|
||
|
||
1. **工具白名单**:Read/Glob/Grep/WebFetch 等只读工具零成本放行(对照 Claude Code 的 `SAFE_YOLO_ALLOWLISTED_TOOLS`)
|
||
2. **编辑快速路径**:Write/Edit 目标在工作区内直接放行(对照 acceptEdits fast path),工作区外交审查
|
||
3. **命令白名单**:只读 shell 命令(git status、ls、cat…)放行;复合命令(含 `&&`、`|`、`$()`)不享受白名单,避免管道尾夹带危险别名
|
||
4. **灾难黑名单**:删盘、格式化、递归删除根目录、fork 炸弹等——确定性拦截,不依赖审查模型存活
|
||
5. **审查模型**:把「任务上下文 + 近期动作 + 待审动作」发给审查模型,二值判定(与 Claude Code 同款):
|
||
- `shouldBlock: false` → 自动放行
|
||
- `shouldBlock: true` → 拒绝执行,理由回传主模型(含逃生舱指引)
|
||
|
||
每次决策写入 `gatekeeper/state/audit.jsonl`(自动轮转 5MB)。
|
||
|
||
## 两种使用模式(按 ZCode 权限模式选择)
|
||
|
||
**确认模式**(`fullAccessGuard: false`,默认):你自己是审批人。ZCode 的权限弹窗照常出现由你确认;审查员负责在弹窗前拦下灾难命令、并按需对灰区操作做任务感知复核。
|
||
|
||
**完全访问模式**(`fullAccessGuard: true`):没有弹窗环节,审查员直接上岗——每次灰区工具调用都经 LLM 任务感知审查:放行 → 静默执行;越权 → 拒绝并回传理由;拿不准 → 按 `onUncertain` 配置处理。灾难黑名单全程兜底。改完 config.json 立即生效(无需重启 ZCode),也可用环境变量 `GATEKEEPER_FULL_ACCESS_GUARD=1` 临时开启。
|
||
|
||
## 拿不准怎么办?(onUncertain 三选一)
|
||
|
||
审查模型对某个调用拿不准时,落地方式由你决定:
|
||
|
||
```jsonc
|
||
"onUncertain": "deny" // 默认:硬拒,理由回传主模型(无人值守安全,不会被弹窗卡死)
|
||
"onUncertain": "prompt" // 升级为 ZCode 原生弹窗问你,你点允许才执行
|
||
"onUncertain": "allow" // 拿不准也放行(最流畅,也最信任审查员)
|
||
```
|
||
|
||
> `prompt` 依赖 ZCode 对 PreToolUse `ask` 决策的支持(已在 ZCode 桌面版实测通过,2026-09)。旧版本若不支持会**静默放行**(fail-open),所以模板默认 `deny`;开启前建议用一条命中黑名单的 echo 命令实测:弹窗出现即可放心使用。
|
||
|
||
**注意:明确的 deny 与灾难黑名单永远硬拒,不受此开关影响**——弹窗只留给"拿不准",人不在场时 agent 不会被卡死。
|
||
|
||
## 被 deny 了但确实想执行?(逃生舱)
|
||
|
||
1. **灰区命令(审查模型判的)**:在对话里明确说"我批准执行此命令"——审查员从键盘捕获通道(模型无法伪造)看到你的明确授权后,重试时放行。
|
||
2. **灾难黑名单拦截的**:对话内口头批准**无效**(防伪造底线),在终端运行拒绝理由中给出的命令:
|
||
|
||
```bash
|
||
node ~/.zcode/gatekeeper/gatekeeper.mjs approve "原命令原样粘贴"
|
||
node ~/.zcode/gatekeeper/gatekeeper.mjs approve --last # 或直接批准最近一条被拒命令
|
||
```
|
||
|
||
一次性放行:精确匹配原命令、用后即焚、10 分钟过期、全程审计。批准后让模型重试即可。
|
||
|
||
> `approve` 设计给**用户亲自执行**:策略提示词明令模型不得自行运行 approve 给自己放行;非交互环境必须追加 `--yes`(留给你明确授权的场景)。
|
||
|
||
切换 ZCode 到确认模式**不能**绕过拦截——灾难黑名单在弹窗之前就已生效(设计意图:任何模式下都不放行灾难命令)。
|
||
|
||
## 配置参考(config.json)
|
||
|
||
```jsonc
|
||
{
|
||
"baseUrl": "…", // 任意 OpenAI 兼容端点
|
||
"apiKey": "…",
|
||
"model": "…", // 建议用与主力模型异构的快速模型(跨模型视角更客观)
|
||
"llmTimeoutMs": 12000, // 单次审查请求超时
|
||
"retries": 1, // 失败重试次数
|
||
"maxOutputTokens": 2000, // 推理模型需要余量,否则判定 JSON 被截断
|
||
"onReviewerError": "deny",// 审查器故障时:deny(默认,fail-closed) | prompt | allow
|
||
"onUncertain": "deny", // 审查模型拿不准时:deny(默认) | prompt 弹窗 | allow 放行
|
||
"fullAccessGuard": false, // 完全访问模式守卫(见"两种使用模式")
|
||
"allowOutput": "json", // PermissionRequest 放行时输出决策 JSON(跳过弹窗)
|
||
"enableAllowlist": true,
|
||
"enableDangerList": true,
|
||
"maxInputChars": 6000,
|
||
"extraBody": {}, // 深合并进 LLM 请求体的自定义字段(见下)
|
||
"daemon": true, // 守护进程模式(false = 每次内联运行)
|
||
"daemonPort": 47811,
|
||
"toolAllowlist": [], // 追加免审工具名
|
||
"autoMode": { // 自定义规则(非空段整体替换 policy.md 中对应默认段)
|
||
"allow": [], // 应自动放行的动作
|
||
"soft_deny": [], // 应拦截的动作
|
||
"environment": [] // 你的环境说明(帮助审查器决策)
|
||
}
|
||
}
|
||
```
|
||
|
||
环境变量:`GATEKEEPER_BASE_URL` / `GATEKEEPER_API_KEY` / `GATEKEEPER_MODEL` / `GATEKEEPER_FULL_ACCESS_GUARD` / `GATEKEEPER_ON_UNCERTAIN`(内联模式直接生效;daemon 模式下 `FULL_ACCESS_GUARD` 随请求透传,其余请改 config.json——会热重载)。
|
||
|
||
## 自定义"思考程度"(extraBody)
|
||
|
||
不同厂商控制推理深度的参数五花八门,所以做成**通用注入**:`extraBody` 里的任意 JSON 会深合并进每次审查请求体。例如部分 vLLM 类部署的模型,用下面的配置即可关闭思考(实测判定耗时降低约 60% 且质量不降):
|
||
|
||
```json
|
||
"extraBody": { "chat_template_kwargs": { "enable_thinking": false } }
|
||
```
|
||
|
||
也可以放 `reasoning_effort`、`top_p` 等任何端点支持的参数(不要设置 `messages`)。改完即时生效(daemon 热重载配置)。
|
||
|
||
## 延迟量级(实测参考)
|
||
|
||
| 路径 | 耗时 | 说明 |
|
||
|---|---|---|
|
||
| 白名单/快速路径 | ~150ms | 进程开销主导,无 LLM 调用 |
|
||
| 灰区判定(审查模型关思考) | ~0.7–1.2s | LLM 推理与网络主导 |
|
||
| 灰区判定(思考开启) | ~1.7–2.5s | 推理模型建议关思考 |
|
||
| 决策缓存命中 | ~150–180ms | 相同任务+命令 15 分钟内免审 |
|
||
|
||
## 守护进程(daemon)
|
||
|
||
客户端每次调用先探测 `127.0.0.1:47811`(无副作用 ping),daemon 不在就自动拉起。收益:到审查端点的 TLS 连接常驻、决策缓存与任务状态驻内存、同请求并发去重、配置热重载(客户端每次请求带配置文件 mtime 签名,变了即重载)。
|
||
|
||
```bash
|
||
node gatekeeper/gatekeeper.mjs shutdown-daemon # 停止(下次调用自动重新拉起)
|
||
GATEKEEPER_NO_DAEMON=1 node gatekeeper.mjs … # 强制内联模式(调试用)
|
||
GATEKEEPER_DEBUG=1 node gatekeeper.mjs … # 打印代理通道调试信息
|
||
```
|
||
|
||
## 日常调优
|
||
|
||
- **误拦了正常命令** → 加进 `allowlist.txt`,或写进 `autoMode.allow`
|
||
- **放行了不该放的** → 写进 `autoMode.soft_deny`,或收紧 `policy.md`
|
||
- **想换审查模型** → 改 `config.json` 前三项即可
|
||
- **审查器不稳定想软着陆** → `onReviewerError: "prompt"`(故障时回落原生弹窗而不是拒绝)
|
||
- **查审计** → `tail -f gatekeeper/state/audit.jsonl`
|
||
|
||
## 威胁模型:恶意上游与提示注入
|
||
|
||
如果接入的上游(主模型提供方)是恶意的,它能伪造什么、不能伪造什么:
|
||
|
||
| 通道 | 上游模型能否伪造 | 说明 |
|
||
|---|---|---|
|
||
| 任务上下文(审查员看到的"用户输入") | **不能** | 由 `UserPromptSubmit` hook 直接从用户键盘输入捕获,模型无法写入 |
|
||
| 审查提示词的溯源标记 | **不能** | 策略标明【当前任务】段是键盘原文;`<action>` 内的"用户已同意"字样一律视为注入 |
|
||
| 待审动作的参数内容 | **能** | 模型可以在命令里塞"用户已确认"等伪造授权文字 → 防注入规则:视为注入,直接 deny |
|
||
| 主模型自己的解释/承诺 | **能,但无效** | 策略明确"模型自我陈述不构成授权",且 transcript 只收录 tool_use、不收录模型文本 |
|
||
| `approve` 放行 | **不能** | 终端执行、审计留痕;非交互环境必须 `--yes`,策略禁止模型自行调用 |
|
||
|
||
诚实的技术边界:**LLM 审查层是"判断层",不是"信任边界"**——它是概率性的,极端注入仍可能绕过。真正的安全边界是本地确定性层:灾难黑名单(不经任何模型)+ `approve` 一次性批准(人工终端操作)+ 确认模式的原生弹窗(人类点击)。另外,若审查端点与主模型同属一个恶意上游,任何 LLM 方案都无意义——建议审查员走与主模型不同的可信渠道,且 `onReviewerError: deny` 保证审查员失效时宁可拒绝。
|
||
|
||
## 测试
|
||
|
||
```bash
|
||
bash test.sh
|
||
```
|
||
|
||
覆盖 29 项决策路径:白名单/黑名单拦截、双 shell 兼容、三种 onUncertain 落地、extraBody 注入、approve 一次性放行与用后即焚、daemon 通道冒烟、fail-closed 降级等。
|
||
|
||
## 卸载
|
||
|
||
```bash
|
||
node gatekeeper/gatekeeper.mjs uninstall --purge
|
||
```
|
||
|
||
自动反注册 hooks(只移除本工具的条目)、停止 daemon、删除安装目录;用户配置每次改动前都有自动备份(`~/.zcode/cli/config.json.bak-gk-*`)。之后重启 ZCode 会话即可。
|
||
|
||
## 已知边界
|
||
|
||
- hooks 在**新会话**生效(ZCode 在会话启动时快照 hooks 配置)。
|
||
- `PreToolUse` 硬层不做 LLM 调用(零延迟);LLM 只在需要权限决策或守卫模式时介入。
|
||
- 若 ZCode 未识别放行/ask 决策 JSON(严格 schema 变动),退化行为是"恢复 ZCode 原生弹窗",不会误放行——**例外**是 `onUncertain: "prompt"` 依赖的 ask 决策,不支持的旧版本会静默放行(见上,开启前请实测)。
|
||
- 灾难命令由黑名单确定性拦截,不依赖审查模型存活。
|
||
|
||
## License
|
||
|
||
[Apache License 2.0](LICENSE)
|
||
|
||
|
||
## 致谢
|
||
|
||
- [Linux.do](https://linux.do)
|