自学教程

DeepSeek Harness 防御性编程

在 DeepSeek Harness 插件与工具开发中,防御性编程,是指在编写插件、defineTool 工具、服务 Provider 代码时,提前预判异常、非法输入、权限失效、依赖缺失、模型错误输出等各类意外场景,保证插件不会崩溃、不会越权、不会破坏会话状态,同时给出友好可审计的错误信息。
Agent 场景和普通后端程序有明显区别:LLM 的输出不可预测,参数经常出现格式错误、越界、非法路径;工具调用可能被频繁中断、暂停、热重载。防御性编程就是为了应对这种不确定性。

核心思想:不要信任大模型输出,不要信任外部输入,假设依赖随时失效,任何操作都要做好兜底保护。

一、防御性编程的核心目标

  1. 故障隔离:单个工具异常不能导致整个会话、整个Harness内核崩溃。
  2. 安全兜底:即使模型传入恶意参数,也不能突破权限门禁、沙箱边界。
  3. 可观测:所有异常都写入会话日志、审计事件,方便回溯定位问题。
  4. 优雅降级:依赖服务不可用时,主动返回提示,而不是抛出未捕获异常。
  5. 兼容热重载:插件热加载、卸载时,正确释放资源,避免内存泄漏、句柄残留。

二、输入防御:不信任模型输出(最关键)

大模型生成的工具参数是不可信的,哪怕定义了 JSON Schema,依然会出现类型错误、溢出、注入内容。

1. 强制参数校验(Schema + 业务双重校验)

defineTool 的 JSON Schema 只做基础类型校验,业务层面必须二次校验。
示例:文件读取工具,除基础schema,额外校验路径,防止路径穿越攻击 ../

defineTool({
  name: "read_file",
  description: "读取文件 Read file",
  permission: ["scope:fs.read"],
  inputSchema: {
    type: "object",
    properties: { path: { type: "string" } },
    required: ["path"]
  },
  async invoke(ctx, args) {
    // 防御:规范化路径,阻断路径穿越
    const safePath = path.resolve(ctx.workspace, args.path);
    if (!safePath.startsWith(ctx.workspace)) {
      throw new Error("路径超出工作目录,禁止访问 Path out of workspace");
    }
    // 业务逻辑
  }
})

防御要点:

  • 校验参数长度,防止超长字符串;
  • 数值参数增加上下限,避免溢出;
  • 枚举参数做白名单校验,不使用黑名单;
  • 过滤特殊注入字符。

2. 处理空值、undefined、缺失字段

模型经常漏传参数,代码不能直接取值。
❌ 危险写法:args.content.trim()
✅ 防御写法:先判断是否存在,设置默认值。

三、依赖与服务防御:应对服务缺失、时序异常

基于 Definition / Provider / Consumer 三角色架构,插件依赖的服务可能:未加载、加载失败、被热卸载。

  1. 使用可选注入,不要强制假设服务一定存在
    获取依赖服务时,捕获注入缺失异常,优雅降级。
// 防御:获取服务,捕获不存在场景
const auditService = ctx.injectOptional("audit.service");
if (!auditService) {
  ctx.logger.warn("审计服务未加载 Audit service not available");
}
  1. 区分硬依赖和软依赖
  • 硬依赖:插件必须依赖,缺失则插件禁用;
  • 软依赖:可选服务,缺失功能降级,插件继续运行。
  1. 生命周期安全:setup / dispose 成对
    插件热重载时,必须在 dispose 钩子释放定时器、文件句柄、事件订阅,防止内存泄漏。
export default definePlugin({
  setup(ctx) {
    const timer = setInterval(() => {}, 1000);
    return {
      dispose() {
        clearInterval(timer); // 资源释放,防御内存泄漏
      }
    }
  }
})

四、异常捕获与错误隔离

1. 工具 invoke 内部必须 try/catch

工具执行抛出未捕获异常,会向上传递,影响轮次状态。所有业务逻辑包裹捕获。

async invoke(ctx, args) {
  try {
    // 业务逻辑
  } catch (err) {
    // 写入会话审计日志
    ctx.events.emit("tool:error", { error: err, args });
    // 返回友好错误,不要直接抛出原始堆栈给大模型(防止信息泄露)
    return { success: false, message: "工具执行失败 Tool execution failed" }
  }
}

安全原则:原始异常堆栈只写入本地日志,返回给LLM的信息做脱敏,不泄露服务器路径、内部密钥。

2. 区分可重试异常 & 不可重试异常

  • 临时网络波动:可提示模型重试;
  • 权限拒绝、参数非法:不可重试,直接终止,避免无限循环调用工具。

五、安全防御:配合权限门禁与沙箱

防御性代码不能替代权限门禁、沙箱,二者是互补关系。

  1. 最小权限原则:工具 permission 只声明必需scope,不要一次性申请全部权限。
  2. 二次校验资源边界:即使沙箱做了隔离,代码内部再次校验路径、域名白名单(纵深防御)。
  3. 禁止在插件代码中绕过权限策略:不要硬编码跳过permission校验。
  4. 高危操作增加前置确认检测:代码内主动检测当前审批策略。

纵深防御理念:权限门禁是第一道防线,沙箱第二道,插件内代码校验是第三道。

六、事件系统防御

插件订阅事件时,容易出现:事件重复订阅、回调多次执行、插件卸载后回调继续触发。
防御实践:

  1. 在插件 dispose 内,取消所有事件监听;
  2. 事件回调内增加状态判断,插件停用后不再执行逻辑;
  3. 事件回调内部同样增加 try-catch,单个事件回调异常,不破坏整个事件总线。
setup(ctx) {
  const handler = async (payload) => {
    try {
      // 事件处理逻辑
    } catch(e) {
      ctx.logger.error("事件处理异常 Event handler error", e);
    }
  }
  ctx.events.on("turn:completed", handler);
  return {
    dispose() {
      ctx.events.off("turn:completed", handler); // 卸载事件监听
    }
  }
}

七、并发、中断与轮次状态防御

Agent 轮次可以被暂停、取消、中断(用户停止对话)。
防御要点:

  1. 长耗时工具任务,监听轮次取消信号,及时终止任务;
  2. 共享资源增加锁,防止同一会话并发多次调用工具造成竞态条件;
  3. 不要在全局变量保存会话状态,状态保存在会话上下文ctx内,避免会话之间状态污染。

❌ 错误:全局变量跨会话共享数据
✅ 正确:状态存储在会话上下文、workspace目录。

八、热重载场景专属防御

Harness 支持插件热重载,是非常容易踩坑的场景:

  1. 禁止全局单例缓存;
  2. 每次setup创建资源,dispose彻底销毁;
  3. 避免残留定时器、websocket连接、文件锁;
  4. Provider服务更新时,保证旧实例正常下线,不中断正在运行的会话。

九、日志与审计防御

  1. 日志脱敏:禁止直接打印 API Key、密钥、令牌;
  2. 关键操作(删除、网络请求、文件修改)强制审计事件;
  3. 日志信息粒度适中:记录操作行为,不记录完整敏感负载。

十、防御性编程检查清单(开发自测)

  • [ ] 所有工具入参进行业务校验,防止路径穿越、参数溢出
  • [ ] 所有异步代码包裹 try / catch
  • [ ] 异常返回给模型的信息做脱敏,原始堆栈仅本地日志
  • [ ] 插件 dispose 钩子释放定时器、事件订阅、IO句柄
  • [ ] 依赖服务使用可选注入,处理依赖缺失降级
  • [ ] 不使用全局变量存储会话状态
  • [ ] 高危操作做双重校验(代码+权限门禁)
  • [ ] 事件回调内部捕获异常,防止事件总线崩溃
  • [ ] 长任务监听轮次取消信号
  • [ ] 日志脱敏,不输出密钥凭据

十一、常见误区

  1. “已经开了沙箱,代码就不用校验参数”

错误。沙箱是环境隔离,代码校验是业务逻辑防护,属于纵深防御,二者缺一不可。

  1. “模型一定会按照Schema输出参数”

错误。LLM输出具有不确定性,Schema校验只是基础防护,必须业务层二次校验。

  1. 异常直接把完整堆栈返回给大模型

风险:信息泄露,模型拿到内部系统信息,触发更多危险调用。

小结

DeepSeek Harness 的防御性编程,本质是面向不确定性编程。
大模型输出不可控、插件支持热重载、会话可随时中断,这些特性让普通后端代码的写法不再安全。
防御性编程围绕五大方向:输入校验、依赖容错、异常捕获、资源安全、事件与并发保护;和权限门禁、沙箱、审计日志构成完整安全体系,保证插件稳定、安全、可审计,避免Agent意外行为带来的故障。

标签:

0 条笔记