/** * Audit Service — 审计日志服务(TOOL 层) * * 负责将工具调用、权限检查、错误等审计记录写入 SQLite audit_logs 表。 * 设计原则: * 1. 所有重要操作必须记录 * 2. 日志不可篡改(INSERT-ONLY) * 3. 支持按时间/类型/会话查询 * 4. v0.2.0: 链式哈希防篡改 — 每条日志的 current_hash = SHA256(prev_hash + content) * * @see docs/MetonaAI-Desktop 架构与交互设计.html — 日志分层 * @see docs/生产级通用 AI Agent 智能体桌面应用:完整设计与构建指南.html — 第十章 */ import type Database from 'better-sqlite3'; import { createHash } from 'crypto'; import log from 'electron-log'; export type AuditEventType = 'tool_call' | 'permission_check' | 'error' | 'llm_request' | 'llm_response' | 'session_start' | 'session_end' | 'config_change'; export type AuditActor = 'agent' | 'user' | 'system'; export type AuditOutcome = 'success' | 'denied' | 'error'; export interface AuditEntry { sessionId: string; iteration?: number; eventType: AuditEventType; actor: AuditActor; target: string; details?: Record; outcome?: AuditOutcome; durationMs?: number; } export class AuditService { constructor(private getDB: () => Database.Database) {} /** * 计算链式哈希 * current_hash = SHA256(prev_hash + 所有内容字段) * v0.2.0: 纳入 iteration/outcome/durationMs 字段,使用 JSON.stringify 避免分隔符碰撞 */ private computeHash( prevHash: string, entry: { sessionId: string; iteration: number | null; eventType: string; actor: string; target: string; details: string | null; outcome: string | null; durationMs: number | null; createdAt: number; }, ): string { // 使用 JSON.stringify 避免分隔符碰撞 const content = JSON.stringify({ prevHash, sessionId: entry.sessionId, iteration: entry.iteration, eventType: entry.eventType, actor: entry.actor, target: entry.target, details: entry.details, outcome: entry.outcome, durationMs: entry.durationMs, createdAt: entry.createdAt, }); return createHash('sha256').update(content, 'utf-8').digest('hex'); } /** * 获取最后一条日志的 hash(用于链式哈希计算) */ private getLastHash(): string { const db = this.getDB(); const row = db.prepare('SELECT current_hash FROM audit_logs ORDER BY id DESC LIMIT 1').get() as { current_hash: string | null } | undefined; return row?.current_hash ?? '0000000000000000000000000000000000000000000000000000000000000000'; } /** * 记录审计条目(带链式哈希) */ log(entry: AuditEntry): void { try { const db = this.getDB(); const now = Date.now(); const detailsStr = entry.details ? JSON.stringify(entry.details) : null; const prevHash = this.getLastHash(); const currentHash = this.computeHash(prevHash, { sessionId: entry.sessionId, iteration: entry.iteration ?? null, eventType: entry.eventType, actor: entry.actor, target: entry.target, details: detailsStr, outcome: entry.outcome ?? null, durationMs: entry.durationMs ?? null, createdAt: now, }); db.prepare(` INSERT INTO audit_logs (session_id, iteration, event_type, actor, target, details, outcome, duration_ms, created_at, prev_hash, current_hash) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `).run( entry.sessionId, entry.iteration ?? null, entry.eventType, entry.actor, entry.target, detailsStr, entry.outcome ?? null, entry.durationMs ?? null, now, prevHash, currentHash, ); } catch (error) { // 审计日志写入失败不应影响主流程 log.error('Audit log write failed:', error); } } /** * 记录工具调用 */ logToolCall(params: { sessionId: string; iteration: number; toolName: string; args: Record; outcome: AuditOutcome; result?: unknown; error?: string; durationMs: number; }): void { this.log({ sessionId: params.sessionId, iteration: params.iteration, eventType: 'tool_call', actor: 'agent', target: params.toolName, details: { args: params.args, result: typeof params.result === 'string' ? params.result.slice(0, 1000) : params.result, error: params.error, }, outcome: params.outcome, durationMs: params.durationMs, }); } /** * 记录 LLM 请求 */ logLLMRequest(params: { sessionId: string; iteration: number; provider: string; model: string; tokenCount: number; }): void { this.log({ sessionId: params.sessionId, iteration: params.iteration, eventType: 'llm_request', actor: 'agent', target: `${params.provider}/${params.model}`, details: { tokenCount: params.tokenCount }, }); } /** * 记录 LLM 响应 */ logLLMResponse(params: { sessionId: string; iteration: number; provider: string; model: string; tokenUsage: { input: number; output: number; total: number }; durationMs: number; }): void { this.log({ sessionId: params.sessionId, iteration: params.iteration, eventType: 'llm_response', actor: 'agent', target: `${params.provider}/${params.model}`, details: { tokenUsage: params.tokenUsage }, outcome: 'success', durationMs: params.durationMs, }); } /** * 记录会话开始 */ logSessionStart(sessionId: string): void { this.log({ sessionId, eventType: 'session_start', actor: 'user', target: 'session', }); } /** * 记录会话结束 */ logSessionEnd(params: { sessionId: string; totalIterations: number; totalTokens: number; durationMs: number; terminationReason: string; }): void { this.log({ sessionId: params.sessionId, eventType: 'session_end', actor: 'agent', target: 'session', details: { totalIterations: params.totalIterations, totalTokens: params.totalTokens, terminationReason: params.terminationReason, }, outcome: 'success', durationMs: params.durationMs, }); } /** * 查询审计日志 */ query(filters?: { sessionId?: string; eventType?: AuditEventType; limit?: number; }): Array<{ id: number; session_id: string; iteration: number | null; event_type: string; actor: string; target: string; details: string | null; outcome: string | null; duration_ms: number | null; created_at: number; prev_hash: string | null; current_hash: string | null; }> { const db = this.getDB(); let sql = 'SELECT * FROM audit_logs WHERE 1=1'; const params: unknown[] = []; if (filters?.sessionId) { sql += ' AND session_id = ?'; params.push(filters.sessionId); } if (filters?.eventType) { sql += ' AND event_type = ?'; params.push(filters.eventType); } sql += ' ORDER BY created_at DESC'; if (filters?.limit) { sql += ' LIMIT ?'; params.push(filters.limit); } return db.prepare(sql).all(...params) as Array<{ id: number; session_id: string; iteration: number | null; event_type: string; actor: string; target: string; details: string | null; outcome: string | null; duration_ms: number | null; created_at: number; prev_hash: string | null; current_hash: string | null; }>; } /** * v0.2.0: 验证链式哈希完整性 * * 遍历所有审计日志,重新计算每条记录的 hash,与存储的 current_hash 对比。 * 如果任何一条记录的 hash 不匹配,说明日志已被篡改。 * * @returns 验证结果,包括是否通过、首个篡改位置的 ID */ verifyChain(): { valid: boolean; totalRecords: number; verifiedRecords: number; tamperedId: number | null } { const db = this.getDB(); const rows = db.prepare('SELECT id, session_id, iteration, event_type, actor, target, details, outcome, duration_ms, created_at, prev_hash, current_hash FROM audit_logs ORDER BY id ASC').all() as Array<{ id: number; session_id: string; iteration: number | null; event_type: string; actor: string; target: string; details: string | null; outcome: string | null; duration_ms: number | null; created_at: number; prev_hash: string | null; current_hash: string | null; }>; let prevHash = '0000000000000000000000000000000000000000000000000000000000000000'; let verified = 0; for (const row of rows) { // 跳过旧数据(current_hash 为 NULL,未回填哈希) if (row.current_hash === null) { prevHash = '0'.repeat(64); continue; } const expectedHash = this.computeHash(prevHash, { sessionId: row.session_id, iteration: row.iteration, eventType: row.event_type, actor: row.actor, target: row.target, details: row.details, outcome: row.outcome, durationMs: row.duration_ms, createdAt: row.created_at, }); if (row.current_hash !== expectedHash) { return { valid: false, totalRecords: rows.length, verifiedRecords: verified, tamperedId: row.id }; } prevHash = row.current_hash; verified++; } return { valid: true, totalRecords: rows.length, verifiedRecords: verified, tamperedId: null }; } }