DeepSeek Harness: 防御性编程与事故复盘

最后更新:2026-08-31

Agent 框架的强大意味着错误的影响也大——一个没有清理的定时器可能泄漏内存,一个硬编码的 API Key 可能泄露到日志,一个未验证的工具结果可能导致 Agent 做出错误决策。防御性编程不是可选的,而是必须的。

💡 提示:防御性编程的核心原则是"不信任任何外部输入"——用户输入、工具返回值、API 响应,都需要验证。你的插件崩溃可以接受,但你的插件导致数据丢失或凭据泄露不可接受。

📋 前置知识:已完成 13-effect.md29-sandbox.md

1. 你将学到


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) 审计流程

100%
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 # 检查内存和连接数是否稳定 ---

📖 小节


📝 作业

1. ⭐ 基础题:审查你之前编写的插件代码,对照安全审计清单逐项检查。列出发现的问题和修复方案。

2. ⭐⭐ 进阶题:为一个工具插件添加完整的输入验证——检查参数类型、长度、格式,对 Shell 命令参数做白名单过滤。测试:输入各种非法参数,确认都能返回有意义的错误信息。

3. ⭐⭐⭐ 挑战题:实现一个 ReversibleAction 系统——每个有副作用的操作都创建 ReversibleAction 对象,execute 时保存原始状态,rollback 时恢复。编写一个文件编辑工具,支持回滚:编辑文件后可以调用 undo_last 撤销上一次编辑。测试:连续编辑三次,依次回滚,确认文件恢复到最初状态。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏