DeepSeek Harness: 防御的プログラミングとインシデントレビュー
最終更新:2026-08-31
Agent フレームワークの力が大きいほど、エラーの影響も大きくなります——クリーンアップされないタイマーはメモリリークを、ハードコードされた API キーはログへの漏洩を、未検証のツール結果は Agent の誤った意思決定を引き起こします。防御的プログラミングはオプションではなく、必須です。
💡 ヒント:防御的プログラミングの核心原則は「外部入力を一切信用しない」——ユーザー入力、ツール戻り値、API レスポンスすべてバリデーションが必要。プラグインがクラッシュするのは許容、プラグインがデータ損失や認証情報漏洩を引き起こすのは許容できません。
📋 前提知識:13-effect.md と 29-sandbox.md の完了
1. 学習内容
- 認証情報管理のベストプラクティス
- 結果レポートとバリデーション
- クリーンアップ保証
- 副作用境界
- インシデントレビュー文化
- セキュリティ監査チェックリスト
2. 認証情報管理のベストプラクティス
▶ サンプル 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:
TYPESCRIPT
// ✅ 設定から読み取り
export const Config = Schema.object({
apiKey:Schema.string().required().hidden()
})
export function apply(ctx:Context) {
const apiKey = ctx.config.apiKey
}
// ✅ 環境変数を使用
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}`)
// ✅ カスタムログフィルタ
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. クリーンアップ保証
(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) 副作用の分類
| タイプ | 説明 | 例 | リスク |
|---|---|---|---|
| 読取専用 | 外部状態を変更しない | ファイル読取、DB クエリ | 低 |
| 冪等書込 | 繰り返し実行しても同じ結果 | ファイル作成(存在すれば上書き) | 中 |
| 非冪等書込 | 繰り返し実行で異なる結果 | メール送信、ログ追記 | 高 |
| 破壊的 | 取り消し不可能な操作 | ファイル削除、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
# Incident Review:[タイトル]
## 基本信息
- Date:YYYY-MM-DD
- Impact:[影響を受けた機能/ユーザー]
- Severity:P0/P1/P2
- Handler:[名前]
## タイムライン
- HH:MM — [イベント1]
- HH:MM — [イベント2]
- HH:MM — [修正]
## 根因分析
[5 Whys 分析]
## 修正対応
- 短期:[即時修正]
- 長期:[予防策]
## 教訓
- [教訓1]
- [教訓2]
(2) 一般的なインシデントパターン
| インシデントパターン | 根因 | 予防策 |
|---|---|---|
| API Key 漏洩 | 認証情報がログに出力 | ログサニタイズ |
| メモリリーク | タイマーがクリーンアップされない | ctx.setInterval を使用 |
| データ損失 | 削除操作に確認がない | 承認ポリシー |
| 無限ループ | ツールが相互呼び出し | 呼び出し深度制限 |
| カスケード障害 | 例外が隔離されていない | try-catch + 独立 Fiber |
▶ サンプル 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 -->|No| FIX[問題を修正]
FIX --> SELF
CHECK -->|Yes| REVIEW[チームレビュー]
REVIEW --> APPROVE{レビュー合格?}
APPROVE -->|No| FIX2[コードを修正]
FIX2 --> REVIEW
APPROVE -->|Yes| 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 でラップし、1つの失敗が他を妨げないようにします。
Q インシデントレビューの頻度は?
A P0/P1 インシデントは発生直後に実施。P2 インシデントは定期的にバッチレビュー。インシデントパターンを定期的に(例:月次)レビュー。
Q セキュリティ監査チェックリストはすべてのプラグインに適用される?
A はい。チェックリスト項目は汎用的なセキュリティ要件。異なるプラグインには追加チェックが必要な場合があります(例:DB プラグインの 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 で直前の編集を取り消し。テスト:3回連続編集後、順次ロールバックし、ファイルが元の状態に戻ることを確認。