feat: 升级至 v0.2.1 — 流式渲染修复、安全增强、工具自动执行
流式渲染修复: - runId 机制防止 abort 后旧流事件污染新 run - run lock 防止并发 run 污染引擎状态 - abort race 提前退出工具执行等待 - TERMINATED 状态通过 stateChange 发射 - tool_call_delta 流式参数拼接 + pending 占位替换 - 首轮卡片创建路径统一,traceStep 按 ID 精确匹配 - compressed 事件转发为 toast 通知 安全增强: - ConfirmationHook 支持持久化自动执行(跨会话) - 设置面板新增自动执行工具管理 UI - SandboxManager 双重安全校验 fail-closed - 审计日志链式哈希防篡改 - PromptInjectionDefender 中文注入标记清理 - scanCode 28 模式 + base64/$() 检测 - validatePath realpathSync 防符号链接逃逸 - code-search 使用 execFile 防命令注入 新增工具: - file_editor、code_search、task_manager、diff_viewer 其他: - Agent Loop 加 PARSING/REFLECTING 状态 + 指数退避重试 - MemoryManager TF-IDF 语义检索 - run_command Windows 中文编码修复(chcp 65001) - 版本号 0.2.0 → 0.2.1
This commit is contained in:
@@ -6,12 +6,14 @@
|
||||
* 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';
|
||||
@@ -33,24 +35,79 @@ 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)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
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,
|
||||
entry.details ? JSON.stringify(entry.details) : null,
|
||||
detailsStr,
|
||||
entry.outcome ?? null,
|
||||
entry.durationMs ?? null,
|
||||
Date.now(),
|
||||
now,
|
||||
prevHash,
|
||||
currentHash,
|
||||
);
|
||||
} catch (error) {
|
||||
// 审计日志写入失败不应影响主流程
|
||||
@@ -185,6 +242,8 @@ export class AuditService {
|
||||
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';
|
||||
@@ -217,6 +276,57 @@ export class AuditService {
|
||||
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 };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user