feat: 升级至 v0.3.1 — 全量代码审计修复 + 安全增强

本次升级基于完整代码审查,修复 Critical/High/Medium/Low 四级共 96 项问题,
并通过返工审计修复 10 项遗留问题,tsc 双端类型检查零错误。

Critical (10/10 完成):
- C-4: command.ts 接入 shell-quote 进行 token-level 注入检测,替代原有正则匹配
  可防御 r"m" -rf /、$'rm'、$(echo rm) 等字符串拼接绕过

High (11/11 完成):
- 竞态保护、Promise.allSettled、AbortController 资源泄漏、IPC 参数校验等

Medium (55/55 完成):
- 事务保护、敏感数据脱敏、枚举校验、MUI v9 Stack prop 迁移、
  React 组件 cancelled 标志、类型收窄等

Low (20/20 完成):
- 辅助方法提取(flushToolCallBuffer/scoreAndPushMemory/tryAddColumn 等)
- nanoid 统一替代 Date.now()+Math.random()
- confirm() 替换为 MUI Dialog、useMemo 缓存、魔法数字命名化等

返工审计修复 (10/10 完成):
- L-11: LogsSettings 残留的原生 confirm()/alert() 全部替换为 MUI Dialog/Alert
- M-53: MemoryViewer handleSearch 独立 ref,修复 searching 状态卡死
- M-42: 脱敏短值(length <= 4)泄露修复
- M-47: tasks:update 补全 title/description 类型校验
- L-9: ollama.adapter 非流式路径 nanoid 统一
- M-45: audit:query limit 策略与 memory:listAll 一致化
- SettingsModal handleConfirmRemove 补全 try/catch + loadServers cleanup
- L-15: CommandPalette useMemo 补全 sessions 响应式依赖
- useAgentStream 事件类型补全 seq/timestamp 字段

新增依赖: shell-quote + @types/shell-quote
版本号: 0.3.0 -> 0.3.1
This commit is contained in:
thzxx
2026-07-13 22:36:58 +08:00
parent 4f5f570ac8
commit e4d81d8247
47 changed files with 2247 additions and 475 deletions
+401 -51
View File
@@ -17,14 +17,15 @@ import type { WorkspaceService } from '../services/workspace.service';
import type { ContextBuilder } from '../harness/prompts/context-builder';
import type { AgentLoopEngine } from '../harness/agent-loop';
import type { ToolRegistry } from '../harness/tools/registry';
import type { AuditService } from '../services/audit.service';
import type { AuditService, AuditEventType } from '../services/audit.service';
import type { SessionRecorder } from '../services/session-recorder.service';
import type { MemoryManager } from '../harness/memory/manager';
import type { MemoryManager, MemoryType } from '../harness/memory/manager';
import type { MCPManager } from '../services/mcp-manager.service';
import type { PromptInjectionDefender } from '../harness/security/prompt-injection-defense';
import type { OutputValidator } from '../harness/verification/output-validator';
import type { ConfirmationHook } from '../harness/hooks/confirmation-hook';
import type { MemoryConsolidator } from '../harness/memory/consolidator';
import type { TaskOrchestrator } from '../harness/orchestration/orchestrator';
import type { MetonaMessage, MetonaStreamEvent } from '../harness/types';
import { MetonaErrorCode, MetonaStreamEventType } from '../harness/types';
import type { MetonaError } from '../harness/types';
@@ -51,12 +52,22 @@ export function registerAllIPCHandlers(
outputValidator: OutputValidator,
confirmationHook: ConfirmationHook,
memoryConsolidator: MemoryConsolidator,
orchestrator: TaskOrchestrator,
): void {
// ===== Agent 交互 =====
ipcMain.handle('agent:sendMessage', async (_event, userMessage: MetonaMessage, sessionId: string) => {
log.info('[AGENT] sendMessage:', sessionId, userMessage.content.slice(0, 80));
// M-33 修复: 参数校验,防止 undefined/非字符串导致下游异常
if (!sessionId || typeof sessionId !== 'string') {
log.warn('[AGENT] sendMessage rejected: invalid sessionId');
return { success: false, error: 'Invalid sessionId' };
}
if (!userMessage || typeof userMessage !== 'object' || typeof userMessage.content !== 'string') {
log.warn('[AGENT] sendMessage rejected: invalid userMessage');
return { success: false, error: 'Invalid message format' };
}
log.info('[AGENT] sendMessage:', sessionId, (userMessage.content ?? '').slice(0, 80));
// 发送消息前确保 Adapter 使用最新配置(失败则中止,防止用旧 Provider 的 adapter 发送)
if (!reloadAdapter()) {
@@ -258,10 +269,16 @@ export function registerAllIPCHandlers(
// 只有当有内容、思考内容或工具调用时才保存
if (step.thought.content || step.thought.reasoningContent || toolCallsWithResults?.length) {
// C-6 修复: assistant 消息仅有 tool_calls 时 content 必须为 null(而非空字符串)
// @see project_memory.md — Assistant messages with tool_calls must set content to null
// 空字符串会导致 LLM API 返回 400 错误,必须使用 null
const assistantContent = (toolCallsWithResults?.length && !step.thought.content)
? null
: step.thought.content;
sessionService.saveMessage({
sessionId,
role: 'assistant',
content: step.thought.content,
content: assistantContent,
reasoningContent: step.thought.reasoningContent || undefined,
toolCalls: toolCallsWithResults,
iteration: step.iteration,
@@ -413,44 +430,81 @@ export function registerAllIPCHandlers(
return sessionService.create(title);
});
ipcMain.handle('sessions:rename', async (_event, sessionId, title) => {
// M-34 修复: 统一 sessionId 校验辅助函数
const isValidSessionId = (id: unknown): id is string =>
typeof id === 'string' && id.length > 0 && id.length <= 200;
ipcMain.handle('sessions:rename', async (_event, sessionId: unknown, title: unknown) => {
// M-34 修复: 校验 sessionId 和 title
if (!isValidSessionId(sessionId)) return { success: false, error: 'Invalid sessionId' };
if (typeof title !== 'string' || !title.trim()) return { success: false, error: 'Invalid title' };
return { success: sessionService.rename(sessionId, title) };
});
ipcMain.handle('sessions:delete', async (_event, sessionId) => {
ipcMain.handle('sessions:delete', async (_event, sessionId: unknown) => {
// M-34 修复: 校验 sessionId
if (!isValidSessionId(sessionId)) return { success: false, error: 'Invalid sessionId' };
return { success: sessionService.delete(sessionId) };
});
ipcMain.handle('sessions:getMessages', async (_event, sessionId) => {
ipcMain.handle('sessions:getMessages', async (_event, sessionId: unknown) => {
// M-34 修复: 校验 sessionId
if (!isValidSessionId(sessionId)) return [];
return sessionService.getMessages(sessionId);
});
ipcMain.handle('sessions:pin', async (_event, sessionId, pinned) => {
ipcMain.handle('sessions:pin', async (_event, sessionId: unknown, pinned: unknown) => {
// M-34 修复: 校验 sessionId 和 pinned
if (!isValidSessionId(sessionId)) return { success: false, error: 'Invalid sessionId' };
if (typeof pinned !== 'boolean') return { success: false, error: 'Invalid pinned flag' };
return { success: sessionService.pin(sessionId, pinned) };
});
ipcMain.handle('sessions:archive', async (_event, sessionId, archived) => {
ipcMain.handle('sessions:archive', async (_event, sessionId: unknown, archived: unknown) => {
// M-34 修复: 校验 sessionId 和 archived
if (!isValidSessionId(sessionId)) return { success: false, error: 'Invalid sessionId' };
if (typeof archived !== 'boolean') return { success: false, error: 'Invalid archived flag' };
return { success: sessionService.archive(sessionId, archived) };
});
ipcMain.handle('sessions:deleteMessage', async (_event, messageId) => {
ipcMain.handle('sessions:deleteMessage', async (_event, messageId: unknown) => {
// M-34 修复: 校验 messageId
if (typeof messageId !== 'string' || !messageId) return { success: false, error: 'Invalid messageId' };
return { success: sessionService.deleteMessage(messageId) };
});
ipcMain.handle('sessions:clearMessages', async (_event, sessionId) => {
ipcMain.handle('sessions:clearMessages', async (_event, sessionId: unknown) => {
// M-34 修复: 校验 sessionId
if (!isValidSessionId(sessionId)) return { success: false, error: 'Invalid sessionId' };
return { success: sessionService.clearMessages(sessionId) };
});
ipcMain.handle('sessions:saveTrace', async (_event, sessionId, data) => {
ipcMain.handle('sessions:saveTrace', async (_event, sessionId: unknown, data: unknown) => {
// M-34 修复: 校验 sessionId 和 data
if (!isValidSessionId(sessionId)) return { success: false, error: 'Invalid sessionId' };
// 严格校验 data 结构:traceSteps 必须为数组,tokenUsage 必须存在
if (!data || typeof data !== 'object') return { success: false, error: 'Invalid trace data' };
const traceData = data as { traceSteps?: unknown; tokenUsage?: unknown };
if (!Array.isArray(traceData.traceSteps)) {
return { success: false, error: 'Invalid trace data: traceSteps must be an array' };
}
if (traceData.tokenUsage === undefined) {
return { success: false, error: 'Invalid trace data: tokenUsage is required' };
}
try {
sessionService.saveTraceData(sessionId, data);
sessionService.saveTraceData(sessionId, {
traceSteps: traceData.traceSteps,
tokenUsage: traceData.tokenUsage,
});
return { success: true };
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
ipcMain.handle('sessions:getTrace', async (_event, sessionId) => {
ipcMain.handle('sessions:getTrace', async (_event, sessionId: unknown) => {
// M-34 修复: 校验 sessionId
if (!isValidSessionId(sessionId)) return null;
return sessionService.getTraceData(sessionId);
});
@@ -461,10 +515,34 @@ export function registerAllIPCHandlers(
});
ipcMain.handle('mcp:addServer', async (_event, config: { name: string; transport: string; command?: string; args?: string[]; url?: string }) => {
// M-38 修复: 完整参数校验,防止字段缺失或类型不符导致异常行为
if (!config || typeof config !== 'object') {
return { success: false, error: 'Invalid config' };
}
if (typeof config.name !== 'string' || !config.name.trim()) {
return { success: false, error: 'Server name is required' };
}
// M-5 修复: transport 运行时校验(替代 as 'stdio' | 'sse' 断言)
if (config.transport !== 'stdio' && config.transport !== 'sse') {
return { success: false, error: `Invalid transport: ${config.transport}. Must be 'stdio' or 'sse'` };
}
// stdio 类型必须有 command
if (config.transport === 'stdio' && (typeof config.command !== 'string' || !config.command.trim())) {
return { success: false, error: 'command is required for stdio transport' };
}
// sse 类型必须有合法 url
if (config.transport === 'sse') {
if (typeof config.url !== 'string' || !config.url.trim()) {
return { success: false, error: 'url is required for sse transport' };
}
try { new URL(config.url); } catch {
return { success: false, error: 'Invalid url format' };
}
}
try {
await mcpManager.addServer({
name: config.name,
transport: config.transport as 'stdio' | 'sse',
transport: config.transport, // 已校验,无需断言
command: config.command,
args: config.args,
url: config.url,
@@ -478,6 +556,10 @@ export function registerAllIPCHandlers(
});
ipcMain.handle('mcp:removeServer', async (_event, name: string) => {
// M-38 修复: name 校验
if (typeof name !== 'string' || !name.trim()) {
return { success: false, error: 'Invalid server name' };
}
try {
await mcpManager.removeServer(name);
return { success: true };
@@ -487,6 +569,13 @@ export function registerAllIPCHandlers(
});
ipcMain.handle('mcp:toggleServer', async (_event, name: string, enabled: boolean) => {
// M-38 修复: name 和 enabled 校验
if (typeof name !== 'string' || !name.trim()) {
return { success: false, error: 'Invalid server name' };
}
if (typeof enabled !== 'boolean') {
return { success: false, error: 'Invalid enabled flag' };
}
try {
await mcpManager.toggleServer(name, enabled);
return { success: true };
@@ -497,8 +586,35 @@ export function registerAllIPCHandlers(
// ===== 记忆 =====
ipcMain.handle('db:searchMemories', async (_event, query, options) => {
return memoryManager.search(query, options);
ipcMain.handle('db:searchMemories', async (_event, query: unknown, options: unknown) => {
// M-37 修复: query 和 options 校验
if (typeof query !== 'string' || !query.trim()) {
return [];
}
// 构造合法的搜索选项(仅保留已知字段,强制类型安全)
const VALID_MEMORY_TYPES: readonly MemoryType[] = ['episodic', 'semantic', 'working'];
const searchOptions: { topK?: number; sessionId?: string; type?: MemoryType; minImportance?: number } = {};
if (options && typeof options === 'object') {
const opts = options as Record<string, unknown>;
// topK 限制范围 1-100(防止过大查询)
if (opts.topK !== undefined) {
const topK = Number(opts.topK);
if (Number.isFinite(topK) && topK >= 1 && topK <= 100) {
searchOptions.topK = topK;
} else {
searchOptions.topK = 10; // 默认值
}
}
if (typeof opts.sessionId === 'string') searchOptions.sessionId = opts.sessionId;
// type 必须是合法的 MemoryType 枚举值
if (typeof opts.type === 'string' && (VALID_MEMORY_TYPES as readonly string[]).includes(opts.type)) {
searchOptions.type = opts.type as MemoryType;
}
if (typeof opts.minImportance === 'number' && Number.isFinite(opts.minImportance)) {
searchOptions.minImportance = opts.minImportance;
}
}
return memoryManager.search(query, searchOptions);
});
// ===== 配置 =====
@@ -507,23 +623,58 @@ export function registerAllIPCHandlers(
return configService.get(key);
});
ipcMain.handle('config:set', async (_event, key, value) => {
ipcMain.handle('config:set', async (_event, key: unknown, value: unknown) => {
// M-42 修复: 参数校验 + 敏感数据脱敏
if (typeof key !== 'string' || !key.trim()) {
return { success: false, error: 'Invalid config key' };
}
// value 必须是可序列化的基本类型(string|number|boolean|null
if (value !== null && typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') {
return { success: false, error: 'Invalid config value: must be string, number, boolean, or null' };
}
// C-1 修复: Provider 切换时清空 API key,防止使用不兼容的 key 导致 401 错误
// @see project_memory.md — When switching LLM providers, the API key must be cleared
if (key === 'llm.provider') {
const oldProvider = configService.get<string>('llm.provider') ?? '';
const newProvider = (value as string) ?? '';
if (oldProvider && newProvider && oldProvider !== newProvider) {
configService.set('llm.apiKey', '');
log.info(`[CONFIG] Provider changed (${oldProvider}${newProvider}), API key cleared to prevent incompatible key usage`);
}
}
configService.set(key, value);
// M-42 修复: 敏感配置项脱敏后再写入审计日志,防止 API Key 明文泄露
// 敏感 key 包括 llm.apiKey、llm.fallbackApiKey、agnes.apiKey 等
// 审计补充修复: 短值(length <= 4)时 slice(-4) 会返回整个 value 导致脱敏失效,
// 改为:长值保留后 4 位核对,短值完全掩码为 '***'
const SENSITIVE_KEY_PATTERNS = ['apikey', 'api_key', 'apitoken', 'token', 'secret', 'password'];
const isSensitiveKey = SENSITIVE_KEY_PATTERNS.some(p => key.toLowerCase().includes(p));
const auditValue = isSensitiveKey && typeof value === 'string' && value.length > 0
? (value.length > 4 ? '***' + value.slice(-4) : '***')
: value;
// TOOL 层:记录配置变更
auditService.log({
sessionId: '',
eventType: 'config_change',
actor: 'user',
target: key,
details: { value },
details: { value: auditValue },
outcome: 'success',
});
// LLM 相关配置变更时热重载 Adapter
if (['llm.provider', 'llm.model', 'llm.apiKey', 'llm.baseURL', 'ollama.numCtx'].includes(key)) {
reloadAdapter();
log.info(`[CONFIG] LLM config changed (${key}), adapter reloaded`);
// C-1 修复: reloadAdapter 返回 false 时表示加载失败,需要通知前端
const reloadSuccess = reloadAdapter();
if (!reloadSuccess) {
log.warn(`[CONFIG] Adapter reload failed after ${key} change`);
} else {
log.info(`[CONFIG] LLM config changed (${key}), adapter reloaded`);
}
}
// Agent 配置变更时热更新 Engine
@@ -535,13 +686,17 @@ export function registerAllIPCHandlers(
log.info(`[CONFIG] Agent totalTimeoutMs updated to ${value}`);
} else if (key === 'agent.enableThinking') {
agentLoop.updateConfig({ thinkingEnabled: value as boolean });
// L-18 修复: 同步更新 orchestrator.defaultConfig,确保新建 SubAgent 使用最新配置
orchestrator.updateDefaultConfig({ thinkingEnabled: value as boolean });
log.info(`[CONFIG] Agent thinkingEnabled updated to ${value}`);
} else if (key === 'agent.thinkingEffort') {
agentLoop.updateConfig({ thinkingEffort: value as 'low' | 'medium' | 'high' | 'max' });
orchestrator.updateDefaultConfig({ thinkingEffort: value as 'low' | 'medium' | 'high' | 'max' });
log.info(`[CONFIG] Agent thinkingEffort updated to ${value}`);
} else if (key === 'ollama.numCtx') {
// numCtx 同时更新 Engine 的 contextLength
agentLoop.updateConfig({ contextLength: (value as number) || undefined });
orchestrator.updateDefaultConfig({ contextLength: (value as number) || undefined });
log.info(`[CONFIG] Agent contextLength updated to ${value}`);
} else if (key === 'agent.confirmationTimeoutMs') {
// 工具确认超时变更,即时应用到 ConfirmationHook
@@ -585,12 +740,38 @@ export function registerAllIPCHandlers(
return app.getPath('userData');
});
ipcMain.handle('app:openExternal', async (_event, url) => {
await shell.openExternal(url);
ipcMain.handle('app:openExternal', async (_event, url: unknown) => {
// M-12 修复: URL 协议白名单校验,防止打开 file:///smb:// 等危险协议
// @see main.ts setWindowOpenHandler — 两处安全策略保持一致
if (typeof url !== 'string' || !url) {
return { success: false, error: 'Invalid URL' };
}
try {
const parsed = new URL(url);
const ALLOWED_PROTOCOLS = ['http:', 'https:', 'mailto:'];
if (!ALLOWED_PROTOCOLS.includes(parsed.protocol)) {
log.warn(`[IPC] openExternal blocked: protocol "${parsed.protocol}" not in whitelist`);
return { success: false, error: `Protocol not allowed: ${parsed.protocol}` };
}
await shell.openExternal(url);
return { success: true };
} catch (error) {
log.warn('[IPC] openExternal failed:', (error as Error).message);
return { success: false, error: (error as Error).message };
}
});
ipcMain.handle('app:showItemInFolder', async (_event, path) => {
shell.showItemInFolder(path);
ipcMain.handle('app:showItemInFolder', async (_event, path: unknown) => {
// M-36 修复: path 类型校验,防止非字符串参数
if (typeof path !== 'string' || !path) {
return { success: false, error: 'Invalid path' };
}
try {
shell.showItemInFolder(path);
return { success: true };
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
ipcMain.handle('app:selectFolder', async (_event, defaultPath?: string) => {
@@ -793,6 +974,16 @@ export function registerAllIPCHandlers(
});
ipcMain.handle('tools:toggle', async (_event, toolName: string, enabled: boolean) => {
// M-35 修复: 校验 toolName 合法性,防止配置 key 污染
if (typeof toolName !== 'string' || !toolName || typeof enabled !== 'boolean') {
return { success: false, error: 'Invalid parameters' };
}
// 校验 toolName 在已注册工具列表中(防止写入任意配置 key)
const allTools = toolRegistry.listAllTools();
if (!allTools.some((t) => t.name === toolName)) {
log.warn(`[IPC] tools:toggle rejected: unknown tool "${toolName}"`);
return { success: false, error: `Unknown tool: ${toolName}` };
}
// 工具开关通过配置持久化
configService.set(`tools.${toolName}.enabled`, enabled);
// 同步到 ToolRegistry(立即生效)
@@ -828,29 +1019,45 @@ export function registerAllIPCHandlers(
});
ipcMain.handle('data:clearSessions', async () => {
// M-40 修复: 危险操作审计日志,便于追踪异常调用
log.warn('[DATA] DANGER: clearSessions invoked — all sessions and messages will be deleted');
const db = sessionService.getDB();
try {
db.exec('BEGIN');
const msgCount = db.prepare('SELECT COUNT(*) as c FROM messages').get() as { c: number };
const sessCount = db.prepare('SELECT COUNT(*) as c FROM sessions').get() as { c: number };
db.exec('DELETE FROM messages');
db.exec('DELETE FROM sessions');
db.exec('COMMIT');
log.info('[DATA] All sessions cleared');
return { success: true };
log.info(`[DATA] All sessions cleared: ${sessCount.c} sessions, ${msgCount.c} messages deleted`);
return { success: true, deletedSessions: sessCount.c, deletedMessages: msgCount.c };
} catch (error) {
try { db.exec('ROLLBACK'); } catch { /* 忽略回滚错误 */ }
log.error('[DATA] clearSessions failed:', (error as Error).message);
return { success: false, error: (error as Error).message };
}
});
ipcMain.handle('data:clearMemories', async () => {
// M-40 修复: 危险操作审计日志,便于追踪异常调用
log.warn('[DATA] DANGER: clearMemories invoked — all memories will be deleted');
const db = sessionService.getDB();
try {
const db = sessionService.getDB();
// M-41 修复: 三个 DELETE 操作用事务包裹,防止部分失败导致三类记忆数据不一致
const epiCount = db.prepare('SELECT COUNT(*) as c FROM episodic_memories').get() as { c: number };
const semCount = db.prepare('SELECT COUNT(*) as c FROM semantic_memories').get() as { c: number };
const workCount = db.prepare('SELECT COUNT(*) as c FROM working_memories').get() as { c: number };
db.exec('BEGIN');
db.exec('DELETE FROM episodic_memories');
db.exec('DELETE FROM semantic_memories');
db.exec('DELETE FROM working_memories');
log.info('[DATA] All memories cleared');
return { success: true };
db.exec('COMMIT');
log.info(`[DATA] All memories cleared: ${epiCount.c} episodic, ${semCount.c} semantic, ${workCount.c} working memories deleted`);
return { success: true, deletedEpisodic: epiCount.c, deletedSemantic: semCount.c, deletedWorking: workCount.c };
} catch (error) {
// M-41 修复: 失败时回滚事务,确保数据一致性
try { db.exec('ROLLBACK'); } catch { /* 忽略回滚错误 */ }
log.error('[DATA] clearMemories failed:', (error as Error).message);
return { success: false, error: (error as Error).message };
}
});
@@ -915,13 +1122,42 @@ export function registerAllIPCHandlers(
// ===== v0.2.0: 工具确认响应(ConfirmationDialog → 主进程)=====
// 使用 ipcMain.on(渲染进程通过 ipcRenderer.send 发送)
ipcMain.on('tool:confirmationResponse', (_event, data: { toolCallId: string; approved: boolean; remember: boolean; autoExecute?: boolean }) => {
confirmationHook.resolveConfirmation(data.toolCallId, data.approved, data.remember, data.autoExecute ?? false);
log.info(`[CONFIRM] Tool ${data.toolCallId} ${data.approved ? 'approved' : 'denied'}${data.remember ? ' (remembered)' : ''}${data.autoExecute ? ' (autoExecute)' : ''}`);
ipcMain.on('tool:confirmationResponse', (_event, data: unknown) => {
// M-43 修复: 校验 data 结构,防止 undefined/null 导致 TypeError
if (!data || typeof data !== 'object') {
log.warn('[IPC] tool:confirmationResponse rejected: invalid data');
return;
}
const req = data as { toolCallId?: unknown; approved?: unknown; remember?: unknown; autoExecute?: unknown };
if (typeof req.toolCallId !== 'string' || !req.toolCallId) {
log.warn('[IPC] tool:confirmationResponse rejected: invalid toolCallId');
return;
}
if (typeof req.approved !== 'boolean') {
log.warn('[IPC] tool:confirmationResponse rejected: invalid approved');
return;
}
const remember = typeof req.remember === 'boolean' ? req.remember : false;
const autoExecute = typeof req.autoExecute === 'boolean' ? req.autoExecute : false;
confirmationHook.resolveConfirmation(req.toolCallId, req.approved, remember, autoExecute);
log.info(`[CONFIRM] Tool ${req.toolCallId} ${req.approved ? 'approved' : 'denied'}${remember ? ' (remembered)' : ''}${autoExecute ? ' (autoExecute)' : ''}`);
});
// ===== v0.2.0: 持久化自动执行设置 =====
ipcMain.handle('tool:setAutoExecute', async (_event, toolName: string, enabled: boolean) => {
ipcMain.handle('tool:setAutoExecute', async (_event, toolName: unknown, enabled: unknown) => {
// M-44 修复: 校验 toolName 合法性和 enabled 类型,防止配置 key 污染
if (typeof toolName !== 'string' || !toolName) {
return { success: false, error: 'Invalid toolName' };
}
if (typeof enabled !== 'boolean') {
return { success: false, error: 'Invalid enabled (must be boolean)' };
}
// 校验 toolName 在已注册工具列表中(与 tools:toggle 保持一致)
const allTools = toolRegistry.listAllTools();
if (!allTools.some((t) => t.name === toolName)) {
log.warn(`[IPC] tool:setAutoExecute rejected: unknown tool "${toolName}"`);
return { success: false, error: `Unknown tool: ${toolName}` };
}
try {
confirmationHook.setAutoExecute(toolName, enabled);
log.info(`[CONFIRM] Tool ${toolName} autoExecute set to ${enabled}`);
@@ -947,16 +1183,49 @@ export function registerAllIPCHandlers(
}
});
ipcMain.handle('audit:query', async (_event, filters?: { sessionId?: string; eventType?: string; limit?: number }) => {
ipcMain.handle('audit:query', async (_event, filters?: unknown) => {
// M-45 修复: 校验 filters 参数类型和范围
const VALID_AUDIT_EVENT_TYPES: readonly AuditEventType[] = [
'tool_call', 'permission_check', 'error', 'llm_request',
'llm_response', 'session_start', 'session_end', 'config_change',
];
let safeFilters: { sessionId?: string; eventType?: AuditEventType; limit?: number } = {};
if (filters && typeof filters === 'object') {
const f = filters as Record<string, unknown>;
if (typeof f.sessionId === 'string' && f.sessionId) safeFilters.sessionId = f.sessionId;
if (typeof f.eventType === 'string' && f.eventType) {
// 校验 eventType 是否在合法枚举内
if (!(VALID_AUDIT_EVENT_TYPES as readonly string[]).includes(f.eventType)) {
return { success: false, error: `Invalid eventType (must be one of: ${VALID_AUDIT_EVENT_TYPES.join(', ')})` };
}
safeFilters.eventType = f.eventType as AuditEventType;
}
if (f.limit !== undefined) {
const limit = Number(f.limit);
// 审计补充修复: limit 越界时返回 error(与 memory:listAll 策略一致,原为静默降级为 100)
// 限制 1-1000 范围,防止过大查询拖慢性能
if (!Number.isFinite(limit) || limit < 1 || limit > 1000) {
return { success: false, error: 'Invalid limit (must be 1-1000)' };
}
safeFilters.limit = Math.floor(limit);
}
}
try {
return { success: true, data: auditService.query(filters as Parameters<typeof auditService.query>[0]) };
return { success: true, data: auditService.query(safeFilters) };
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
// ===== v0.2.0: 任务管理 IPCTaskList UI=====
ipcMain.handle('tasks:list', async (_event, sessionId?: string) => {
const VALID_TASK_PRIORITIES: readonly string[] = ['low', 'medium', 'high', 'critical'];
const VALID_TASK_STATUSES: readonly string[] = ['pending', 'in_progress', 'completed', 'blocked', 'cancelled'];
ipcMain.handle('tasks:list', async (_event, sessionId?: unknown) => {
// M-46 修复: 校验 sessionId 类型(可选参数)
if (sessionId !== undefined && (typeof sessionId !== 'string' || !sessionId)) {
return { success: false, error: 'Invalid sessionId' };
}
const db = sessionService.getDB();
let sql = 'SELECT * FROM tasks';
const params: unknown[] = [];
@@ -968,33 +1237,83 @@ export function registerAllIPCHandlers(
return { success: true, data: db.prepare(sql).all(...params) };
});
ipcMain.handle('tasks:create', async (_event, data: { sessionId: string; title: string; description?: string; priority?: string; parentId?: string }) => {
ipcMain.handle('tasks:create', async (_event, data: unknown) => {
// M-46 修复: 校验 data 结构和字段类型/枚举
if (!data || typeof data !== 'object') {
return { success: false, error: 'Invalid task data' };
}
const req = data as Record<string, unknown>;
if (typeof req.sessionId !== 'string' || !req.sessionId.trim()) {
return { success: false, error: 'Invalid sessionId' };
}
if (typeof req.title !== 'string' || !req.title.trim()) {
return { success: false, error: 'Invalid title' };
}
if (req.priority !== undefined && !VALID_TASK_PRIORITIES.includes(req.priority as string)) {
return { success: false, error: `Invalid priority (must be one of: ${VALID_TASK_PRIORITIES.join(', ')})` };
}
if (req.parentId !== undefined && req.parentId !== null && typeof req.parentId !== 'string') {
return { success: false, error: 'Invalid parentId' };
}
const db = sessionService.getDB();
const id = `task_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
try {
db.prepare(`
INSERT INTO tasks (id, session_id, title, description, status, priority, parent_id, created_at, updated_at)
VALUES (?, ?, ?, ?, 'pending', ?, ?, ?, ?)
`).run(id, data.sessionId, data.title, data.description ?? '', data.priority ?? 'medium', data.parentId ?? null, Date.now(), Date.now());
`).run(
id,
req.sessionId,
req.title,
typeof req.description === 'string' ? req.description : '',
(req.priority as string) ?? 'medium',
(req.parentId as string | null) ?? null,
Date.now(),
Date.now(),
);
return { success: true, id };
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
ipcMain.handle('tasks:update', async (_event, id: string, updates: { title?: string; description?: string; status?: string; priority?: string; assignedTo?: string }) => {
ipcMain.handle('tasks:update', async (_event, id: unknown, updates: unknown) => {
// M-47 修复: 校验 id 和 updates 结构/枚举
if (typeof id !== 'string' || !id) {
return { success: false, error: 'Invalid task id' };
}
if (!updates || typeof updates !== 'object') {
return { success: false, error: 'Invalid updates' };
}
const u = updates as Record<string, unknown>;
if (u.status !== undefined && !VALID_TASK_STATUSES.includes(u.status as string)) {
return { success: false, error: `Invalid status (must be one of: ${VALID_TASK_STATUSES.join(', ')})` };
}
if (u.priority !== undefined && !VALID_TASK_PRIORITIES.includes(u.priority as string)) {
return { success: false, error: `Invalid priority (must be one of: ${VALID_TASK_PRIORITIES.join(', ')})` };
}
if (u.assignedTo !== undefined && u.assignedTo !== null && typeof u.assignedTo !== 'string') {
return { success: false, error: 'Invalid assignedTo' };
}
// 审计补充修复: 补全 title/description 类型校验(与 tasks:create 的严格校验一致)
if (u.title !== undefined && typeof u.title !== 'string') {
return { success: false, error: 'Invalid title (must be string)' };
}
if (u.description !== undefined && typeof u.description !== 'string') {
return { success: false, error: 'Invalid description (must be string)' };
}
const db = sessionService.getDB();
try {
const fields: string[] = [];
const values: unknown[] = [];
if (updates.title !== undefined) { fields.push('title = ?'); values.push(updates.title); }
if (updates.description !== undefined) { fields.push('description = ?'); values.push(updates.description); }
if (updates.status !== undefined) { fields.push('status = ?'); values.push(updates.status); }
if (updates.priority !== undefined) { fields.push('priority = ?'); values.push(updates.priority); }
if (updates.assignedTo !== undefined) { fields.push('assigned_to = ?'); values.push(updates.assignedTo); }
if (u.title !== undefined) { fields.push('title = ?'); values.push(u.title); }
if (u.description !== undefined) { fields.push('description = ?'); values.push(u.description); }
if (u.status !== undefined) { fields.push('status = ?'); values.push(u.status); }
if (u.priority !== undefined) { fields.push('priority = ?'); values.push(u.priority); }
if (u.assignedTo !== undefined) { fields.push('assigned_to = ?'); values.push(u.assignedTo); }
if (fields.length === 0) return { success: true };
fields.push('updated_at = ?'); values.push(Date.now());
if (updates.status === 'completed') { fields.push('completed_at = ?'); values.push(Date.now()); }
if (u.status === 'completed') { fields.push('completed_at = ?'); values.push(Date.now()); }
values.push(id);
db.prepare(`UPDATE tasks SET ${fields.join(', ')} WHERE id = ?`).run(...values);
return { success: true };
@@ -1003,7 +1322,11 @@ export function registerAllIPCHandlers(
}
});
ipcMain.handle('tasks:delete', async (_event, id: string) => {
ipcMain.handle('tasks:delete', async (_event, id: unknown) => {
// M-48 修复: 校验 id 类型
if (typeof id !== 'string' || !id) {
return { success: false, error: 'Invalid task id' };
}
const db = sessionService.getDB();
try {
db.prepare('DELETE FROM tasks WHERE id = ?').run(id);
@@ -1014,10 +1337,30 @@ export function registerAllIPCHandlers(
});
// ===== v0.2.0: 记忆系统增强查询(Memory Viewer UI=====
ipcMain.handle('memory:listAll', async (_event, options?: { type?: string; limit?: number }) => {
const VALID_MEMORY_TYPES_LIST: readonly string[] = ['episodic', 'semantic', 'working'];
ipcMain.handle('memory:listAll', async (_event, options?: unknown) => {
// M-49 修复: 校验 options.type 枚举和 limit 范围
let limit = 100;
let type: string | undefined;
if (options && typeof options === 'object') {
const opts = options as Record<string, unknown>;
if (opts.type !== undefined) {
if (typeof opts.type !== 'string' || !VALID_MEMORY_TYPES_LIST.includes(opts.type)) {
return { success: false, error: `Invalid type (must be one of: ${VALID_MEMORY_TYPES_LIST.join(', ')})` };
}
type = opts.type;
}
if (opts.limit !== undefined) {
const num = Number(opts.limit);
// 限制 1-1000 范围,SQLite 中 LIMIT -1 表示无限制,需阻止
if (!Number.isFinite(num) || num < 1 || num > 1000) {
return { success: false, error: 'Invalid limit (must be 1-1000)' };
}
limit = Math.floor(num);
}
}
const db = sessionService.getDB();
const limit = options?.limit ?? 100;
const type = options?.type;
const results: Record<string, unknown[]> = {};
try {
if (!type || type === 'episodic') {
@@ -1038,7 +1381,14 @@ export function registerAllIPCHandlers(
}
});
ipcMain.handle('memory:delete', async (_event, type: string, id: string) => {
ipcMain.handle('memory:delete', async (_event, type: unknown, id: unknown) => {
// M-50 修复: 校验 type 枚举(防止三元表达式默认映射到 working_memories)和 id 类型
if (typeof type !== 'string' || !VALID_MEMORY_TYPES_LIST.includes(type)) {
return { success: false, error: `Invalid type (must be one of: ${VALID_MEMORY_TYPES_LIST.join(', ')})` };
}
if (typeof id !== 'string' || !id) {
return { success: false, error: 'Invalid memory id' };
}
const db = sessionService.getDB();
try {
const table = type === 'episodic' ? 'episodic_memories' : type === 'semantic' ? 'semantic_memories' : 'working_memories';