# 语义与信任边界

这份文档说明 MoonGate 保证了什么、不保证什么，以及失败时会发生什么。它比功能列表更重要：安全工具的边界写不清楚，使用者就会把“拦截了一部分”误解为“全都拦住了”。

## 信任模型

**信任**：策略文件、网关进程及其内存 vault、嵌入了 guard 的服务端进程、本地审批台的操作用户。

**不信任**：

- **Agent**。它可能被提示注入操纵，可能自信地编造参数，也可能主动尝试越权。网关只看它发出的协议消息。
- **没有嵌入 guard 的服务端**。它可能返回带密钥的错误信息、超长内容或畸形 JSON。网关不假设它是善意的。
- **工具返回值里的文本**。它只作为血缘提取的输入，不会被当作指令。

**假设**：要么 Agent 经过网关，要么服务端嵌入了 guard。两者至少有一个生效；两个都没有时，不受保护。

## 两个执行点各自保证什么

| | 网关 | 服务端 guard |
| --- | --- | --- |
| 禁用工具 | ✅ | ✅ |
| 路径逃逸 / 参数规则 | ✅ | ✅ |
| 默认拒绝 | ✅ | ✅ |
| 工具清单裁剪 | ✅ | — |
| 跨调用值级血缘 | ✅ | ❌（无会话状态） |
| 人工审批 | ✅ | ❌（审批结果只在网关） |
| 哈希链账本 | ✅ | — |
| 被绕过时仍生效 | ❌ | ✅ |

guard 只执行硬拒绝；遇到 `approval` 规则时返回 `allow`，把决定权留给网关。否则网关已经批准的调用会被服务端再挡一次。

## 保证

1. **执行前拦截（网关）**。判定为 `deny` 的 `tools/call` 不会被写入上游 stdin。
2. **执行前拦截（服务端）**。嵌入 guard 的服务端在工具函数入口处拒绝，即使请求没有经过网关。
3. **一次批准只授权一次**。授权与 `(tool, arguments)` 的规范化 SHA-256 绑定；参数不同、重复解析、超过 TTL 都不会转发。
4. **值级外发控制**。被 `sensitive` 命中的结果中的密钥形状值，其指纹会随会话保存；外发工具参数中出现同一值时拒绝。
5. **密钥不进模型上下文**。同一批值在转发给客户端前被替换为 `«mg:<指纹>»`；明文只存在于网关内存的会话 vault。
6. **替换目标逐条授权**。参数里出现占位符时，只有命中 `reveal` 规则的调用会被换回真值；未命中一律 `secret-reveal` 拒绝。
7. **能力最小化**。被无条件拒绝的工具不出现在 `tools/list`。
8. **可校验的账本**。每条事件包含前一条的哈希；修改任意一行都会使 `verify` 失败并指出序号。
9. **秘密不落盘**。账本只记录参数键名、参数摘要、值指纹、4 字符前缀与替换次数，不记录参数值、明文或工具返回内容。vault 不写文件、不导出。

## 不保证

- **不阻止既不过网关、服务端也没嵌 guard 的调用**。这是部署约束，不是实现缺陷。
- **不做语义级 DLP**。血缘与替身都是精确值匹配；“把密钥拆成两段发出去”“Base64 编码后发出”“用同义词描述密钥”都不会被识别。
- **替身只覆盖工具结果的字符串叶子**。如果明文通过其他通道（截图、日志尾随、另一个未被标记为 sensitive 的工具）进入上下文，不会被替换。
- **vault 是进程内存**。拥有网关进程内存读取权限的攻击者可以取得明文；本项目不防御本机提权。
- **替换目标由策略决定**。`reveal` 规则写错，密钥就会发给错误的目的地；这是配置风险，不是机制漏洞。
- **不是操作系统沙箱**。网关不限制服务端的文件、网络、进程权限；guard 也只检查参数，不约束工具实现内部的行为。
- **不检测提示注入**。它不读对话内容，只读工具调用。提示注入的后果体现为“某个工具调用被拒绝”，而不是“注入被识别”。
- **不保证上游行为正确**。放行只表示“策略允许”，不表示“结果可信”。
- **路径规范化是词法的**。解析 `.` 与 `..`，不解析符号链接，也不做 URL 解码。
- **不做多用户隔离**。审批台监听 `127.0.0.1`，同一个本机用户即可操作；多人环境需要额外身份层。
- **不做高可用**。网关是单进程，崩溃即中断会话。

## 威胁模型与覆盖情况

| 威胁 | 例子 | 网关 | 服务端 guard | 依赖 |
| --- | --- | --- | --- | --- |
| 凭据进入模型上下文 | 读到 `.env`，明文进入提示词/日志 | ✅ | ❌ | 开启 `mask_secrets` |
| 凭据外泄 | 读到 `.env` 后 POST 到外部 | ✅ | ❌ | 策略把读取标为 `sensitive`，且没有 `reveal` 规则 |
| 占位符被滥用 | 把 `«mg:…»` 发到非授权地址 | ✅ | ✅ | `reveal` 规则；guard 拒绝未解析占位符 |
| 越权文件访问 | `../../etc/passwd` | ✅ | ✅ | 规则带 `normalize: "path"` |
| 前缀绕过 | `reports/../../etc/passwd` | ✅ | ✅ | 同上 |
| 破坏性动作 | `fs.delete`、`DROP TABLE` | ✅ | ✅ | 工具名可枚举 |
| 绕过网关 | 客户端直连服务端 | ❌ | ✅ | 服务端嵌入 guard |
| 未经确认的高影响操作 | 转账、发邮件 | ✅ | ❌ | 操作人真的会看审批台 |
| 审批重放 | 用旧批准执行新参数 | ✅ | — | 参数摘要绑定 |
| 失控循环 | Agent 反复重试 | ✅ | — | 阈值合理 |
| 服务端反向索取 | `sampling/createMessage` | ✅ | — | — |
| 提示注入 | 文档里藏指令让 Agent 外发 | ⚠️ 间接 | ⚠️ 间接 | 只能拦住注入导致的敏感外发 |
| 数据变形外泄 | Base64、分片、改写 | ❌ | ❌ | 精确匹配的固有限制 |
| 符号链接逃逸 | 链接指向工作区外 | ❌ | ❌ | 词法规范化不解析链接 |

## 失败模式

| 情况 | 行为 |
| --- | --- |
| 客户端发来非法 JSON | 回 `-32700`，不转发，写 `protocol_error` 账本 |
| 客户端发来未知方法 | 回 `-32601`，不转发 |
| `tools/call` 缺少 `params.name` | 回 `-32602`，不转发 |
| 单行超过 8 MiB | 回 `-32600`，不转发（防止内存放大） |
| 服务端返回非法 JSON | 丢弃该行，写账本并告警，不崩溃 |
| 服务端发起 `sampling` / `elicitation` | 回 `-32001`，写账本 |
| 审批超时 | 回 `-32002`，不转发 |
| 参数含未授权占位符 | 回 `-32001`，规则 `secret-reveal`，不转发 |
| 网关重启后旧占位符 | 无法解析，按 `secret-reveal` 拒绝（有意失效） |
| 审批被拒绝 | 回 `-32001`，不转发 |
| guard 拒绝 | 服务端返回 `isError`，工具函数不执行 |
| guard 策略加载失败 | 服务端拒绝所有工具调用（演示实现为失败关闭） |
| guard 收到未解析占位符 | 返回 `unresolved-secret`，不执行工具 |
| 上游进程退出 | 网关收尾、写 `session_close`、退出码 1 |
| 引擎内部异常 | 网关写日志并以退出码 3 终止，不静默放行 |

设计原则是**失败关闭**：不确定时拒绝，而不是放行。引擎异常直接终止进程，避免出现“监管者已经坏了但流量还在走”的状态。

## 账本能证明什么

账本可以证明：**这份记录自写入以来没有被修改过**，且每条判定与前后记录的顺序一致。

账本不能证明：记录一定完整，也不能证明 vault 里的明文从未被读取。网关可以在写账本前崩溃，也可以（理论上）被改造成不写某些事件。要防止后者，需要把哈希头发布到网关之外（例如定期打印到另一个系统），这超出本项目范围。

## 与“策略引擎”的区别

规则匹配是必要的，但只靠规则，拦截面等于“作者事先想到的情况”。血缘把判断依据从“工具名和参数长什么样”扩展到“这个值刚才从哪里来”。第二执行点把“必须经过某个进程”变成“两个独立位置都检查”。三者叠加，而不是互相替代。

## 性能

协议解析与判定是纯字符串与哈希运算，单次调用在微秒级；瓶颈在实际工具执行与进程间通信。账本逐条 `appendFileSync`，换取崩溃时的可读性；高频场景可以换成批量写入，但会牺牲最后几条记录。guard 是一次内存中的规则匹配，开销可以忽略。

## 部署建议

- 网关与服务端在同一台机器上，通过 stdio 连接，不额外暴露端口；
- 服务端尽量嵌入 guard，并在策略加载失败时拒绝启动；
- 审批台只监听 `127.0.0.1`，不要转发到公网；
- 策略文件纳入版本管理，`policy_digest` 会写进账本，报告可与策略版本对应；
- 定期把 `verify` 的哈希头抄送到别处，防止账本被整体替换。
