在 DeepSeek Harness 插件与工具开发中,防御性编程,是指在编写插件、defineTool 工具、服务 Provider 代码时,提前预判异常、非法输入、权限失效、依赖缺失、模型错误输出等各类意外场景,保证插件不会崩溃、不会越权、不会破坏会话状态,同时给出友好可审计的错误信息。
Agent 场景和普通后端程序有明显区别:LLM 的输出不可预测,参数经常出现格式错误、越界、非法路径;工具调用可能被频繁中断、暂停、热重载。防御性编程就是为了应对这种不确定性。
核心思想:不要信任大模型输出,不要信任外部输入,假设依赖随时失效,任何操作都要做好兜底保护。
一、防御性编程的核心目标
- 故障隔离:单个工具异常不能导致整个会话、整个Harness内核崩溃。
- 安全兜底:即使模型传入恶意参数,也不能突破权限门禁、沙箱边界。
- 可观测:所有异常都写入会话日志、审计事件,方便回溯定位问题。
- 优雅降级:依赖服务不可用时,主动返回提示,而不是抛出未捕获异常。
- 兼容热重载:插件热加载、卸载时,正确释放资源,避免内存泄漏、句柄残留。
二、输入防御:不信任模型输出(最关键)
大模型生成的工具参数是不可信的,哪怕定义了 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 三角色架构,插件依赖的服务可能:未加载、加载失败、被热卸载。
- 使用可选注入,不要强制假设服务一定存在
获取依赖服务时,捕获注入缺失异常,优雅降级。
// 防御:获取服务,捕获不存在场景
const auditService = ctx.injectOptional("audit.service");
if (!auditService) {
ctx.logger.warn("审计服务未加载 Audit service not available");
}
- 区分硬依赖和软依赖
- 硬依赖:插件必须依赖,缺失则插件禁用;
- 软依赖:可选服务,缺失功能降级,插件继续运行。
- 生命周期安全: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. 区分可重试异常 & 不可重试异常
- 临时网络波动:可提示模型重试;
- 权限拒绝、参数非法:不可重试,直接终止,避免无限循环调用工具。
五、安全防御:配合权限门禁与沙箱
防御性代码不能替代权限门禁、沙箱,二者是互补关系。
- 最小权限原则:工具
permission只声明必需scope,不要一次性申请全部权限。 - 二次校验资源边界:即使沙箱做了隔离,代码内部再次校验路径、域名白名单(纵深防御)。
- 禁止在插件代码中绕过权限策略:不要硬编码跳过permission校验。
- 高危操作增加前置确认检测:代码内主动检测当前审批策略。
纵深防御理念:权限门禁是第一道防线,沙箱第二道,插件内代码校验是第三道。
六、事件系统防御
插件订阅事件时,容易出现:事件重复订阅、回调多次执行、插件卸载后回调继续触发。
防御实践:
- 在插件 dispose 内,取消所有事件监听;
- 事件回调内增加状态判断,插件停用后不再执行逻辑;
- 事件回调内部同样增加 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 轮次可以被暂停、取消、中断(用户停止对话)。
防御要点:
- 长耗时工具任务,监听轮次取消信号,及时终止任务;
- 共享资源增加锁,防止同一会话并发多次调用工具造成竞态条件;
- 不要在全局变量保存会话状态,状态保存在会话上下文ctx内,避免会话之间状态污染。
❌ 错误:全局变量跨会话共享数据
✅ 正确:状态存储在会话上下文、workspace目录。
八、热重载场景专属防御
Harness 支持插件热重载,是非常容易踩坑的场景:
- 禁止全局单例缓存;
- 每次setup创建资源,dispose彻底销毁;
- 避免残留定时器、websocket连接、文件锁;
- Provider服务更新时,保证旧实例正常下线,不中断正在运行的会话。
九、日志与审计防御
- 日志脱敏:禁止直接打印 API Key、密钥、令牌;
- 关键操作(删除、网络请求、文件修改)强制审计事件;
- 日志信息粒度适中:记录操作行为,不记录完整敏感负载。
十、防御性编程检查清单(开发自测)
- [ ] 所有工具入参进行业务校验,防止路径穿越、参数溢出
- [ ] 所有异步代码包裹 try / catch
- [ ] 异常返回给模型的信息做脱敏,原始堆栈仅本地日志
- [ ] 插件 dispose 钩子释放定时器、事件订阅、IO句柄
- [ ] 依赖服务使用可选注入,处理依赖缺失降级
- [ ] 不使用全局变量存储会话状态
- [ ] 高危操作做双重校验(代码+权限门禁)
- [ ] 事件回调内部捕获异常,防止事件总线崩溃
- [ ] 长任务监听轮次取消信号
- [ ] 日志脱敏,不输出密钥凭据
十一、常见误区
- “已经开了沙箱,代码就不用校验参数”
错误。沙箱是环境隔离,代码校验是业务逻辑防护,属于纵深防御,二者缺一不可。
- “模型一定会按照Schema输出参数”
错误。LLM输出具有不确定性,Schema校验只是基础防护,必须业务层二次校验。
- 异常直接把完整堆栈返回给大模型
风险:信息泄露,模型拿到内部系统信息,触发更多危险调用。
小结
DeepSeek Harness 的防御性编程,本质是面向不确定性编程。
大模型输出不可控、插件支持热重载、会话可随时中断,这些特性让普通后端代码的写法不再安全。
防御性编程围绕五大方向:输入校验、依赖容错、异常捕获、资源安全、事件与并发保护;和权限门禁、沙箱、审计日志构成完整安全体系,保证插件稳定、安全、可审计,避免Agent意外行为带来的故障。
0 条笔记