DeepSeek Harness: 防御性编程与事故复盘
最后更新:2026-08-31
Agent 框架的强大意味着错误的影响也大——一个没有清理的定时器可能泄漏内存,一个硬编码的 API Key 可能泄露到日志,一个未验证的工具结果可能导致 Agent 做出错误决策。防御性编程不是可选的,而是必须的。
💡 提示:防御性编程的核心原则是"不信任任何外部输入"——用户输入、工具返回值、API 响应,都需要验证。你的插件崩溃可以接受,但你的插件导致数据丢失或凭据泄露不可接受。
📋 前置知识:已完成 13-effect.md 和 29-sandbox.md
1. 你将学到
- 凭据管理最佳实践
- 结果报告与验证
- 清理保证(cleanup guarantee)
- 副作用边界
- 事故复盘文化
- 安全审计清单
2. 凭据管理最佳实践
(1) ▶ 示例 1
TYPESCRIPT
// ❌ 硬编码 API Key
const apiKey = 'sk-abc123def456'
// ❌ API Key 写入日志
ctx.logger.info(`connecting with key: ${apiKey}`)
// ❌ API Key 放在 URL 中
const url = `https://api.example.com?key=${apiKey}`
// ❌ API Key 放在错误消息中
throw new Error(`Authentication failed for key: ${apiKey}`)
(2) ▶ 示例 2
TYPESCRIPT
// ✅ 从配置读取
export const Config = Schema.object({
apiKey: Schema.string().required().hidden()
})
export function apply(ctx: Context) {
const apiKey = ctx.config.apiKey
// apiKey 只在 apply 内使用,不泄露到外部
}
// ✅ 使用环境变量
const apiKey = process.env.MY_PLUGIN_API_KEY
// ✅ 请求头传递(不进入 URL)
const response = await fetch(url, {
headers: { 'Authorization': `Bearer ${apiKey}` }
})
(3) 日志中的凭据保护
TYPESCRIPT
// ✅ 日志中隐藏敏感信息
ctx.logger.info(`connecting to ${endpoint}`) // 不记录 key
// ✅ 自定义日志过滤器
function sanitize(obj: any): any {
const sanitized = { ...obj }
if (sanitized.apiKey) sanitized.apiKey = '***'
if (sanitized.authorization) sanitized.authorization = '***'
return sanitized
}
ctx.logger.info('request:', sanitize(request))
(4) 凭据轮换
TYPESCRIPT
export const Config = Schema.object({
apiKey: Schema.string().required().hidden(),
keyRotationDays: Schema.number().default(90)
})
export function apply(ctx: Context) {
ctx.setInterval(() => {
const age = getKeyAge()
if (age > ctx.config.keyRotationDays * 86400000) {
ctx.logger.warn('API key is overdue for rotation')
}
}, 86400000)
}
3. 结果报告与验证
(1) 验证工具返回值
TYPESCRIPT
// ❌ 不验证工具结果
async execute({ path }, ctx) {
const result = await ctx.shell.execute(`ls ${path}`)
return result.stdout // 可能为空或格式异常
}
// ✅ 验证工具结果
async execute({ path }, ctx) {
const result = await ctx.shell.execute(`ls ${path}`)
if (result.exitCode !== 0) {
return {
error: true,
message: `ls failed: ${result.stderr}`,
exitCode: result.exitCode
}
}
if (!result.stdout || result.stdout.trim().length === 0) {
return {
error: true,
message: 'Directory is empty or does not exist'
}
}
const files = result.stdout.trim().split('\n')
return { count: files.length, files }
}
(2) 验证 API 响应
TYPESCRIPT
// ❌ 不验证 API 响应
const data = await response.json()
return data.result
// ✅ 验证 API 响应
async function callAPI(ctx: Context, endpoint: string): Promise<any> {
const response = await fetch(endpoint)
if (!response.ok) {
throw new Error(`API error: ${response.status} ${response.statusText}`)
}
let data: any
try {
data = await response.json()
} catch {
throw new Error('API returned invalid JSON')
}
if (!data || typeof data !== 'object') {
throw new Error('API returned unexpected format')
}
return data
}
(3) 输入验证
TYPESCRIPT
// ❌ 不验证用户输入
async execute({ command }, ctx) {
return await ctx.shell.execute(command) // 命令注入风险
}
// ✅ 验证和清理输入
async execute({ command }, ctx) {
if (!command || typeof command !== 'string') {
return { error: true, message: 'Invalid command' }
}
if (command.length > 1000) {
return { error: true, message: 'Command too long' }
}
// 白名单检查
const allowed = ['ls', 'cat', 'grep', 'wc', 'head', 'tail']
const baseCommand = command.split(' ')[0]
if (!allowed.includes(baseCommand)) {
return { error: true, message: `Command not allowed: ${baseCommand}` }
}
return await ctx.shell.execute(command)
}
4. 清理保证(cleanup guarantee)
(1) 清理保证原则
无论插件如何退出(正常卸载、异常崩溃、手动停止),所有资源必须被清理。
(2) 清理清单
| 资源类型 | 清理方式 | 保证机制 |
|---|---|---|
| 定时器 | ctx.setInterval/setTimeout | 自动清理 |
| 事件监听 | ctx.on() | 自动清理 |
| 网络连接 | ctx.effect() | 手动注册 |
| 临时文件 | ctx.effect() | 手动注册 |
| 子进程 | ctx.effect() | 手动注册 |
| 全局状态 | ctx.effect() | 手动注册 |
(3) 清理保证模式
TYPESCRIPT
export function apply(ctx: Context) {
// 所有需要清理的资源
const resources: { close: () => void | Promise<void> }[] = []
// 获取资源时立即注册清理
function acquireResource<T extends { close: () => void | Promise<void> }>(
resource: T
): T {
resources.push(resource)
return resource
}
// 统一清理
ctx.effect(() => {
return async () => {
for (const r of resources.reverse()) {
try {
await r.close()
} catch (e) {
ctx.logger.warn('cleanup error:', e)
}
}
}
})
// 使用
const db = acquireResource(openDatabase())
const ws = acquireResource(new WebSocket('ws://localhost:8080'))
}
(4) 临时文件清理
TYPESCRIPT
export function apply(ctx: Context) {
const tempFiles: string[] = []
ctx.effect(() => {
return async () => {
for (const file of tempFiles.reverse()) {
try {
await ctx.fs.unlink(file)
} catch {}
}
}
})
async function createTempFile(content: string): Promise<string> {
const path = `/tmp/dsh-${Date.now()}-${Math.random().toString(36).slice(2)}`
await ctx.fs.writeFile(path, content)
tempFiles.push(path)
return path
}
}
5. 副作用边界
(1) 副作用分类
| 类型 | 说明 | 示例 | 风险 |
|---|---|---|---|
| 只读 | 不修改外部状态 | 读取文件、查询数据库 | 低 |
| 幂等写入 | 可重复执行效果一致 | 创建文件(已存在则覆盖) | 中 |
| 非幂等写入 | 重复执行效果不同 | 发送邮件、追加日志 | 高 |
| 破坏性 | 不可逆操作 | 删除文件、DROP TABLE | 极高 |
(2) 副作用边界原则
TEXT
📖 仅展示
原则 1: 副作用最小化
→ 只执行必要的操作
→ 优先只读,其次幂等写入
原则 2: 副作用可回滚
→ 写入前保存原始状态
→ 提供撤销操作
原则 3: 副作用可审计
→ 记录每次副作用的详情
→ 用户可以查看历史操作
原则 4: 副作用需审批
→ 破坏性操作必须审批
→ 非幂等操作建议审批
(3) 回滚模式
TYPESCRIPT
interface ReversibleAction {
execute(): Promise<void>
rollback(): Promise<void>
}
class FileEditAction implements ReversibleAction {
private originalContent: string | null = null
constructor(
private path: string,
private newContent: string,
private ctx: Context
) {}
async execute() {
try {
this.originalContent = await this.ctx.fs.readFile(this.path)
} catch {
this.originalContent = null
}
await this.ctx.fs.writeFile(this.path, this.newContent)
}
async rollback() {
if (this.originalContent !== null) {
await this.ctx.fs.writeFile(this.path, this.originalContent)
} else {
await this.ctx.fs.unlink(this.path)
}
}
}
(4) 副作用审计
TYPESCRIPT
const auditLog: AuditEntry[] = []
function audit(action: string, details: any, reversible: boolean) {
auditLog.push({
timestamp: Date.now(),
action,
details,
reversible,
user: 'agent'
})
ctx.emit('audit/action', { action, details, reversible })
}
// 使用
audit('file_edit', { path: 'src/app.ts', operation: 'edit' }, true)
audit('email_send', { to: 'alice@example.com' }, false)
6. 事故复盘文化
(1) 事故复盘模板
MARKDOWN
# 事故复盘: [标题]
## 基本信息
- 日期: YYYY-MM-DD
- 影响范围: [受影响的功能/用户]
- 严重程度: P0/P1/P2
- 处理人: [姓名]
## 时间线
- HH:MM — [事件1]
- HH:MM — [事件2]
- HH:MM — [修复]
## 根因分析
[5 Whys 分析]
## 修复措施
- 短期: [立即修复]
- 长期: [预防措施]
## 教训
- [教训1]
- [教训2]
(2) 常见事故模式
| 事故模式 | 根因 | 预防 |
|---|---|---|
| API Key 泄露 | 日志中打印凭据 | 日志脱敏 |
| 内存泄漏 | 定时器未清理 | 使用 ctx.setInterval |
| 数据丢失 | 删除操作无确认 | 审批策略 |
| 无限循环 | 工具互相调用 | 调用深度限制 |
| 级联故障 | 异常未隔离 | try-catch + 独立 Fiber |
(3) ▶ 示例 3
TEXT
📖 仅展示
事故: Agent 删除了用户项目文件
Why 1: Agent 执行了 rm -rf /project
→ 因为工具没有路径验证
Why 2: 工具没有路径验证
→ 因为开发者没有实现白名单检查
Why 3: 开发者没有实现白名单检查
→ 因为没有安全审查流程
Why 4: 没有安全审查流程
→ 因为团队没有建立安全清单
Why 5: 团队没有建立安全清单
→ 因为缺乏安全意识培训
修复: 建立安全审计清单,所有插件发布前必须检查
7. 安全审计清单
(1) 插件安全审计清单
| # | 检查项 | 类别 | 优先级 |
|---|---|---|---|
| 1 | API Key 不硬编码 | 凭据 | P0 |
| 2 | 敏感信息不出现在日志中 | 凭据 | P0 |
| 3 | 所有 ctx.effect 有清理函数 | 清理 | P0 |
| 4 | 定时器用 ctx.setInterval/setTimeout | 清理 | P0 |
| 5 | 工具返回值有错误处理 | 验证 | P1 |
| 6 | 用户输入有验证和清理 | 验证 | P1 |
| 7 | API 响应格式有验证 | 验证 | P1 |
| 8 | 破坏性操作有审批策略 | 副作用 | P1 |
| 9 | 非幂等操作可回滚 | 副作用 | P2 |
| 10 | 副作用有审计日志 | 副作用 | P2 |
| 11 | 权限声明完整 | 权限 | P1 |
| 12 | 无不必要的权限请求 | 权限 | P2 |
| 13 | 依赖版本有兼容性声明 | 兼容 | P2 |
| 14 | 无已知安全漏洞的依赖 | 依赖 | P1 |
(2) 审计流程
graph TD
CODE[插件开发完成] --> SELF[开发者自审]
SELF --> CHECK{清单全部通过?}
CHECK -->|否| FIX[修复问题]
FIX --> SELF
CHECK -->|是| REVIEW[团队审查]
REVIEW --> APPROVE{审查通过?}
APPROVE -->|否| FIX2[修改代码]
FIX2 --> REVIEW
APPROVE -->|是| PUBLISH[发布]
(3) 自动化审计
BASH
# 运行安全审计
dsh audit my-plugin
# 输出
🔒 Security Audit: my-plugin
✅ No hardcoded credentials
✅ All ctx.effect() have cleanup functions
⚠️ Tool 'db_query' has no input validation
❌ API response not validated in 'fetch_data'
✅ Approval policy configured for destructive operations
⚠️ Permission 'shell.execute' may not be necessary
2 errors, 2 warnings found. Fix before publishing.
❓ 常见问题
Q 防御性编程会让代码变慢吗?
A 验证逻辑的运行时开销通常可以忽略不计。安全问题的修复成本远高于预防成本。
Q 每个工具都需要审批吗?
A 不需要。只读操作可以
always 允许。需要审批的是有副作用的操作(写文件、执行命令、发送请求)。Q 如何处理不可恢复的清理失败?
A 记录错误日志,继续清理其他资源。Cordis 内部对每个清理函数都有 try-catch,一个失败不会阻止其他清理。
Q 事故复盘需要多频繁?
A 每次 P0/P1 事故后立即复盘。P2 事故可以积累后批量复盘。定期(如每月)回顾事故模式。
Q 安全审计清单适用于所有插件吗?
A 是的。清单中的项目是通用的安全要求。不同插件可能需要额外检查(如数据库插件的 SQL 注入防护)。
Q 如何测试清理保证?
A 反复加载/卸载插件,监控资源使用:
bash # 循环测试 for i in {1..100}; do dsh plugin enable my-plugin dsh plugin disable my-plugin done # 检查内存和连接数是否稳定 ---📖 小节
- 凭据管理:不硬编码、不入日志、用配置/环境变量、定期轮换
- 结果验证:验证工具返回值、API 响应、用户输入,不信任外部数据
- 清理保证:所有资源注册清理函数,统一管理,异常不阻断其他清理
- 副作用边界:最小化副作用、可回滚、可审计、破坏性操作需审批
- 事故复盘:5 Whys 分析根因,建立修复措施和预防机制
- 安全审计清单:14 项检查,开发者自审 + 团队审查 + 自动化扫描
📝 作业
1. ⭐ 基础题:审查你之前编写的插件代码,对照安全审计清单逐项检查。列出发现的问题和修复方案。
2. ⭐⭐ 进阶题:为一个工具插件添加完整的输入验证——检查参数类型、长度、格式,对 Shell 命令参数做白名单过滤。测试:输入各种非法参数,确认都能返回有意义的错误信息。
3. ⭐⭐⭐ 挑战题:实现一个 ReversibleAction 系统——每个有副作用的操作都创建 ReversibleAction 对象,execute 时保存原始状态,rollback 时恢复。编写一个文件编辑工具,支持回滚:编辑文件后可以调用 undo_last 撤销上一次编辑。测试:连续编辑三次,依次回滚,确认文件恢复到最初状态。