Skip to content

6.10 Fail-closed 设计 — 默认「不安全」与工厂函数

前置阅读6.2 Tool 接口 · 6.9 MCP


学习目标

完成本节学习后,你应该能够:

  1. 解释 isConcurrencySafe=falseisReadOnly=false 作为默认值的 fail-closed 含义。
  2. 说明 为何「未显式声明安全」的工具应视为需串行或需锁
  3. 设计 工厂函数 defineTool(),强制调用方传入安全能力位,避免遗漏。
  4. 对照 只读搜索工具与 Bash 工具在安全位上的典型覆盖。
  5. 关联 权限 UI:默认拦截、显式放行与审计日志。

生活类比:化工管道默认阀门

Fail-closed 像常闭电磁阀断电(信息缺失)= 停止流动。若你不知道一条管道是单行道还是双向,默认按会爆炸处理——关阀赌运气便宜。

isConcurrencySafe 默认 false:不知道能否并行,就不要并行isReadOnly 默认 false:不知道是否改状态,就按会改状态要求授权。


两个布尔位的语义(表)

字段truefalse(默认 fail-closed)
isReadOnly不写盘、不改外部系统可能写盘或副作用 → 需严格策略
isConcurrencySafe可并行调用无竞态可能有竞态 → 串行或加锁

为何默认「不安全」

场景若错误标 readOnly: true若错误标 concurrencySafe: true
文件写绕过写权限交错写入损坏
MCP 工具静默改远端数据重复扣款
Bash极危管道竞态

原则:只有可证明的性质才能标 true;缺失证明 → 保守默认。


源码片段:危险默认值(反例)

typescript
// 反模式:默认值过于乐观
interface ToolDefBad {
  isReadOnly?: boolean; // 缺省 undefined → 被当成 falsy 仍可能逻辑错误
  isConcurrencySafe?: boolean;
}

function runBad(def: ToolDefBad) {
  const readOnly = def.isReadOnly ?? true; // 若有人误用「缺省即只读」会灾难
  // ...
}

源码片段:工厂函数强制显式(推荐)

typescript
type SafetyFlags =
  | { isReadOnly: true; isConcurrencySafe: true }
  | { isReadOnly: true; isConcurrencySafe: false }
  | { isReadOnly: false; isConcurrencySafe: false };

interface ToolConfig<I, O> extends SafetyFlags {
  name: string;
  inputSchema: ZodSchema<I>;
  outputSchema: ZodSchema<O>;
  call(input: I): Promise<O>;
}

function defineTool<I, O>(config: ToolConfig<I, O>) {
  // 编译期:必须传入 SafetyFlags 联合的某一枝,不能省略
  return {
    ...config,
    // 运行期再次断言,防止 as any 逃逸
    meta: {
      isReadOnly: config.isReadOnly,
      isConcurrencySafe: config.isConcurrencySafe,
    },
  };
}

// 示例:只读且可并发
const globTool = defineTool({
  name: "Glob",
  isReadOnly: true,
  isConcurrencySafe: true,
  inputSchema: GlobInput,
  outputSchema: GlobOutput,
  call: async () => ({ paths: [] }),
});

// 示例:写操作必须 isReadOnly: false,通常 isConcurrencySafe: false
const fileEditTool = defineTool({
  name: "FileEdit",
  isReadOnly: false,
  isConcurrencySafe: false,
  inputSchema: FileEditInput,
  outputSchema: FileEditOutput,
  call: async () => ({ applied: true }),
});

Mermaid:调度器如何消费安全位


与治理流水线的衔接

步骤fail-closed 影响
权限决策非只读 → 默认弹窗
并行优化非 concurrencySafe → 禁止合并
缓存非只读 → 禁止缓存响应

MCP 与第三方扩展

对 MCP 工具:

  • 禁止信任对端声明的 readOnly 除非有本地证明(静态列表或人工勾选);
  • 默认 isConcurrencySafe: false
  • 可在 registerMcpServer覆盖为更保守。

测试矩阵(概念表)

工具isReadOnlyisConcurrencySafe
Globtruetrue
Greptruetrue(视实现,可能 false)
FileReadtruetrue
FileEditfalsefalse
Bashfalsefalse
MCP 未知falsefalse

常见反模式

反模式后果
「大部分是只读」默认 true写穿权限
为性能盲目并行数据竞争
用注释代替布尔位调度器读不到

审计与 UI

事件记录
tool_safety_flags注册时快照
override管理员改标记需留痕

小结

  • Fail-closed = 缺省最保守 + 证明才可放宽
  • 工厂函数 + 联合类型 能在编译期减少遗漏。
  • 调度与权限 应消费同一套元数据,避免分叉逻辑。

自测题

  1. isReadOnly=true 但实现里写了日志文件,属于什么失败模式?
  2. 何时 isReadOnly=true 仍不应 isConcurrencySafe=true
  3. 如何把 fail-closed 策略做成可配置的企业策略包?

上一节6.9 MCP · 下一节6.11 延迟加载

本项目仅用于教育学习目的。Claude Code 源码版权归 Anthropic, PBC 所有。