evolcore 0.0.15 → 0.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -484,13 +484,19 @@ function analyzeShellProtectedOperands(command, options) {
484
484
  analysis,
485
485
  hClass: containsHClassReference(command),
486
486
  lClass: containsLClassReference(command),
487
+ pathHClass: false,
488
+ lockDiagnostic: false,
487
489
  };
488
490
  }
489
491
  const operands = queryPathOperands(analysis);
492
+ const lockDiagnostic = /(?:^|[\/\s'"`])(?:[^\s'"`/]*\.lock|EVOLCORE_RUNTIME_LOCK_DIR)(?=$|[\/\s'"`=])/i.test(command);
493
+ const pathHClass = operands.some(({ operand, accessKind }) => shellOperandMatchesClass(operand, 'h', options, accessKind));
490
494
  return {
491
495
  analysis,
492
- hClass: operands.some(({ operand, accessKind }) => shellOperandMatchesClass(operand, 'h', options, accessKind)),
496
+ hClass: lockDiagnostic || pathHClass,
493
497
  lClass: operands.some(({ operand, accessKind }) => shellOperandMatchesClass(operand, 'l', options, accessKind)),
498
+ pathHClass,
499
+ lockDiagnostic,
494
500
  };
495
501
  }
496
502
  function protectedReadToolPaths(toolName, input) {
@@ -586,6 +592,45 @@ function readonlyShellStaysInWorkspace(analysis, projectPath, managedTempDir) {
586
592
  || (!!managedTemp && isSameOrDescendant(candidate, managedTemp));
587
593
  })());
588
594
  }
595
+ const READONLY_SOURCE_ROOTS = new Set([
596
+ 'src', 'lib', 'app', 'apps', 'packages', 'package', 'scripts',
597
+ 'test', 'tests', 'ecagent', 'ecweb',
598
+ ]);
599
+ /**
600
+ * Optional source-diagnostic capability. It is deliberately opt-in and only
601
+ * admits bounded, proven-readonly queries rooted at a named source directory;
602
+ * project root, home, dependency, config, and session trees remain outside it.
603
+ */
604
+ function readonlySourceDiagnosticAllowed(analysis, projectPath) {
605
+ if (analysis.kind !== 'proven-readonly')
606
+ return false;
607
+ const workspace = resolveProtectedCandidate(projectPath);
608
+ const operands = queryPathOperands(analysis);
609
+ if (operands.length === 0)
610
+ return false;
611
+ return operands.every(({ operand, accessKind }) => {
612
+ if (accessKind === 'metadata')
613
+ return false;
614
+ const candidate = resolveProtectedCandidate(operand.value, projectPath);
615
+ if (!isSameOrDescendant(candidate, workspace))
616
+ return false;
617
+ const relative = path.relative(workspace, candidate);
618
+ const segments = relative.split(path.sep).filter(Boolean);
619
+ const first = segments[0];
620
+ if (!first || !READONLY_SOURCE_ROOTS.has(first) || segments.includes('node_modules'))
621
+ return false;
622
+ return !containsHClassReference(operand.value) && !containsLClassReference(operand.value)
623
+ && !containsLockPathReference(operand.value)
624
+ && !isHClassPath(candidate, { root: resolveRoot() })
625
+ && !isLClassPath(candidate, { root: resolveRoot() });
626
+ });
627
+ }
628
+ /** A lock filename used as a query pattern is safe; an operand naming the
629
+ * actual lock path is still protected. */
630
+ function containsLockPathReference(value) {
631
+ return /(?:^|[\\/])(?:[^\\/]*\.lock|EVOLCORE_RUNTIME_LOCK_DIR)(?:$|[\\/])/i.test(value)
632
+ || /^(?:[^\\/]*\.lock|EVOLCORE_RUNTIME_LOCK_DIR)$/i.test(value);
633
+ }
589
634
  /**
590
635
  * 只读模式检查(用于 PreToolUse hook 和 canUseTool callback)。普通
591
636
  * 路径上的受限查询 Bash 可自动执行;H/L-class 和未建模 Shell 仍拒绝。
@@ -631,6 +676,13 @@ export function checkReadonly(toolName, input, projectPath, context) {
631
676
  managedTempDir,
632
677
  allowProtectedMetadata: context?.allowProtectedMetadata,
633
678
  });
679
+ const sourceDiagnosticAllowed = context?.allowReadonlySourceDiagnostics
680
+ && readonlySourceDiagnosticAllowed(protectedOperands.analysis, projectPath)
681
+ && !protectedOperands.lClass
682
+ && (!protectedOperands.hClass || (protectedOperands.lockDiagnostic && !protectedOperands.pathHClass));
683
+ if (sourceDiagnosticAllowed) {
684
+ return { behavior: 'allow' };
685
+ }
634
686
  if (protectedOperands.analysis.kind === 'proven-readonly'
635
687
  && !protectedOperands.hClass
636
688
  && !protectedOperands.lClass
@@ -929,7 +981,12 @@ export function checkHClassWrite(toolName, input, context) {
929
981
  managedTempDir: context?.managedTempDir,
930
982
  allowProtectedMetadata: context?.allowProtectedMetadata,
931
983
  });
932
- if (protectedOperands.hClass) {
984
+ const sourceDiagnosticAllowed = context?.permissionMode === 'readonly'
985
+ && context.allowReadonlySourceDiagnostics === true
986
+ && readonlySourceDiagnosticAllowed(protectedOperands.analysis, context.projectPath ?? process.cwd())
987
+ && !protectedOperands.lClass
988
+ && (!protectedOperands.hClass || (protectedOperands.lockDiagnostic && !protectedOperands.pathHClass));
989
+ if (protectedOperands.hClass && !sourceDiagnosticAllowed) {
933
990
  logger.warn(`[H-Class Protection] 🔒 Protected filesystem operand in shell command: tool=${toolName} ` +
934
991
  `session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
935
992
  return {
@@ -1398,6 +1455,8 @@ export function evaluateToolPreflight(toolName, input, context) {
1398
1455
  workspacePath: context.workspacePath,
1399
1456
  root: context.root,
1400
1457
  managedTempDir: context.managedTempDir,
1458
+ permissionMode: context.permissionMode,
1459
+ allowReadonlySourceDiagnostics: context.allowReadonlySourceDiagnostics,
1401
1460
  allowProtectedMetadata: context.allowProtectedMetadata,
1402
1461
  });
1403
1462
  if (hClass.behavior === 'deny') {
package/dist/index.js CHANGED
@@ -227,18 +227,21 @@ export function resolvePersistedRestartDelivery(opts) {
227
227
  ? { valid: true, delivery: opts.delivery }
228
228
  : { valid: false };
229
229
  }
230
- function daemonConversationWatchdogMs(settings) {
230
+ function daemonIdleTimeoutMs(settings) {
231
231
  const idleTimeoutSec = settings.idleMonitor?.timeout;
232
- const idleMs = typeof idleTimeoutSec === 'number' && Number.isFinite(idleTimeoutSec) && idleTimeoutSec > 0
232
+ return typeof idleTimeoutSec === 'number' && Number.isFinite(idleTimeoutSec) && idleTimeoutSec > 0
233
233
  ? idleTimeoutSec * 1000
234
234
  : 120_000;
235
+ }
236
+ function daemonConversationWatchdogMs(settings) {
237
+ const idleMs = daemonIdleTimeoutMs(settings);
235
238
  return Math.ceil(idleMs * 5 + 60_000);
236
239
  }
237
240
  function daemonConversationTotalExecutionMs(settings) {
238
241
  const seconds = settings.idleMonitor?.maxExecutionTime;
239
242
  return typeof seconds === 'number' && Number.isFinite(seconds) && seconds > 0
240
243
  ? seconds * 1000
241
- : 60 * 60 * 1000;
244
+ : undefined;
242
245
  }
243
246
  function originFromActorSession(session, authenticatedPeerId, authenticatedChannelKey, authenticatedChatType) {
244
247
  const peerId = authenticatedPeerId || session.metadata?.peerId;
@@ -1227,6 +1230,7 @@ async function main() {
1227
1230
  // 回填 messageQueue 引用
1228
1231
  cmdHandler.setMessageQueue(messageQueue);
1229
1232
  processor.setMessageQueue(messageQueue);
1233
+ const handoffQueueTtlMs = daemonIdleTimeoutMs(globalSettings);
1230
1234
  const taskExecutionTtlMs = daemonConversationTotalExecutionMs(globalSettings);
1231
1235
  const handoffRuntime = new HandoffRuntime(sessionManager, messageQueue, async (handoff) => {
1232
1236
  const targetSession = await sessionManager.getSessionById(handoff.target_session_id);
@@ -1277,7 +1281,7 @@ async function main() {
1277
1281
  })
1278
1282
  : { ok: false, error: 'AUN channel does not support daemon private sends' };
1279
1283
  return { ok: result.ok, message_id: result.message_id, error: result.error };
1280
- }, undefined, { queueTtlMs: taskExecutionTtlMs });
1284
+ }, undefined, { queueTtlMs: handoffQueueTtlMs });
1281
1285
  responseEngine.setHandoffRuntime(handoffRuntime);
1282
1286
  cmdHandler.setHandoffRuntime(handoffRuntime);
1283
1287
  // Trigger runtime: daemon-level script + feedback scheduler.
package/dist/ipc.js CHANGED
@@ -1047,6 +1047,7 @@ export class IpcServer {
1047
1047
  * Query the running EvolCore daemon via Unix socket.
1048
1048
  * Returns null if the service is not running or the socket is unreachable.
1049
1049
  */
1050
+ /** Pass `null` as timeoutMs to wait without a client-side deadline. */
1050
1051
  export function ipcQuery(socketPath, cmd, timeoutMs = 3000) {
1051
1052
  let authenticatedCmd = cmd;
1052
1053
  if (!process.env.EVOLCORE_SESSION_ID
@@ -1067,10 +1068,12 @@ export function ipcQuery(socketPath, cmd, timeoutMs = 3000) {
1067
1068
  return new Promise((resolve) => {
1068
1069
  const conn = net.connect(socketPath);
1069
1070
  let buf = '';
1070
- const timer = setTimeout(() => {
1071
- conn.destroy();
1072
- resolve(null);
1073
- }, timeoutMs);
1071
+ const timer = timeoutMs === null
1072
+ ? undefined
1073
+ : setTimeout(() => {
1074
+ conn.destroy();
1075
+ resolve(null);
1076
+ }, timeoutMs);
1074
1077
  conn.on('connect', () => {
1075
1078
  conn.write(JSON.stringify(request) + '\n');
1076
1079
  });
@@ -1078,7 +1081,8 @@ export function ipcQuery(socketPath, cmd, timeoutMs = 3000) {
1078
1081
  buf += data.toString();
1079
1082
  const idx = buf.indexOf('\n');
1080
1083
  if (idx !== -1) {
1081
- clearTimeout(timer);
1084
+ if (timer)
1085
+ clearTimeout(timer);
1082
1086
  try {
1083
1087
  resolve(JSON.parse(buf.slice(0, idx)));
1084
1088
  }
@@ -1089,7 +1093,8 @@ export function ipcQuery(socketPath, cmd, timeoutMs = 3000) {
1089
1093
  }
1090
1094
  });
1091
1095
  conn.on('error', () => {
1092
- clearTimeout(timer);
1096
+ if (timer)
1097
+ clearTimeout(timer);
1093
1098
  resolve(null);
1094
1099
  });
1095
1100
  });
package/dist/paths.js CHANGED
@@ -169,7 +169,24 @@ function resolveInstanceSocketPath(root) {
169
169
  const hash = crypto.createHash('sha1').update(root).digest('hex').slice(0, 12);
170
170
  return `\\\\.\\pipe\\${WINDOWS_PIPE_PREFIX}-${hash}`;
171
171
  }
172
- return path.join(root, 'data', 'instance', INSTANCE_SOCKET_FILENAME);
172
+ const filesystemPath = path.join(root, 'data', 'instance', INSTANCE_SOCKET_FILENAME);
173
+ // Linux/macOS Unix-domain sockets have a small sockaddr path limit
174
+ // (typically 108 bytes on Linux). Long managed TMPDIR prefixes can make a
175
+ // perfectly valid EVOLCORE_HOME impossible to bind. Keep the fallback a
176
+ // normal filesystem socket whenever possible: the endpoint is passed to
177
+ // child processes through environment variables, which cannot contain NUL
178
+ // bytes. The short path remains inside the managed temporary directory and
179
+ // uses the runtime root hash for isolation.
180
+ if (Buffer.byteLength(filesystemPath) < 100)
181
+ return filesystemPath;
182
+ const hash = crypto.createHash('sha1').update(root).digest('hex').slice(0, 20);
183
+ const shortFilesystemPath = path.join(os.tmpdir(), `.ec-${hash.slice(0, 15)}.sock`);
184
+ if (Buffer.byteLength(shortFilesystemPath) < 100)
185
+ return shortFilesystemPath;
186
+ // This is only reachable when the managed temporary directory itself is
187
+ // unusually long. Node's in-process IPC can use the abstract namespace;
188
+ // callers that cross a process boundary must provide an encoded endpoint.
189
+ return `\0evolcore-${hash}`;
173
190
  }
174
191
  export function ensureDataDirs() {
175
192
  const p = resolvePaths();
@@ -1064,9 +1064,10 @@ export class TriggerRuntimeScheduler {
1064
1064
  && !!run.attemptId
1065
1065
  && typeof run.executionSessionId === 'string'
1066
1066
  && !!run.executionSessionId
1067
- && typeof run.executionDeadlineAt === 'number'
1068
- && Number.isFinite(run.executionDeadlineAt)
1069
- && run.executionDeadlineAt > Date.now()
1067
+ && (run.executionDeadlineAt === undefined
1068
+ || (typeof run.executionDeadlineAt === 'number'
1069
+ && Number.isFinite(run.executionDeadlineAt)
1070
+ && run.executionDeadlineAt > Date.now()))
1070
1071
  && run.definitionRevision === definitionRevision(definition);
1071
1072
  }
1072
1073
  async continueRecoveredRun(definition, runtime, session) {
@@ -1668,7 +1669,7 @@ export class TriggerRuntimeScheduler {
1668
1669
  return this.running.get(triggerId)?.has(runId) === true;
1669
1670
  }
1670
1671
  daemonExecutionDeadlineAt(startedAt) {
1671
- return this.daemonChannel.executionDeadlineAt?.(startedAt) ?? startedAt + 60 * 60 * 1_000;
1672
+ return this.daemonChannel.executionDeadlineAt?.(startedAt);
1672
1673
  }
1673
1674
  clearTimer(triggerId) {
1674
1675
  const timer = this.timers.get(triggerId);
@@ -4,6 +4,7 @@ import { execFileSync, execFile, spawn, spawnSync } from 'child_process';
4
4
  import { promisify } from 'util';
5
5
  import fs from 'fs';
6
6
  import { getProcessStartTime, parseCimDate } from './process-introspect.js';
7
+ import { decodeWindowsOutput } from './windows-output.js';
7
8
  const execFileAsync = promisify(execFile);
8
9
  export const isWindows = process.platform === 'win32';
9
10
  /**
@@ -287,30 +288,6 @@ function parseDateString(value) {
287
288
  const parsed = Date.parse(value);
288
289
  return Number.isNaN(parsed) ? null : parsed;
289
290
  }
290
- /** Decode PowerShell output consistently across Windows 5.1 and pwsh. */
291
- function decodeWindowsOutput(value) {
292
- if (!value)
293
- return '';
294
- const bytes = Buffer.isBuffer(value) ? value : Buffer.from(value);
295
- if (bytes.length >= 2 && bytes[0] === 0xff && bytes[1] === 0xfe) {
296
- return bytes.subarray(2).toString('utf16le');
297
- }
298
- if (bytes.length >= 2 && bytes[0] === 0xfe && bytes[1] === 0xff) {
299
- const swapped = Buffer.allocUnsafe(bytes.length - 2);
300
- for (let i = 2; i + 1 < bytes.length; i += 2) {
301
- swapped[i - 2] = bytes[i + 1];
302
- swapped[i - 1] = bytes[i];
303
- }
304
- return swapped.toString('utf16le');
305
- }
306
- const utf8 = bytes.toString('utf8').replace(/^\uFEFF/, '');
307
- if (utf8.includes('\u0000')) {
308
- const utf16 = bytes.toString('utf16le').replace(/^\uFEFF/, '');
309
- if (utf16.includes('{') || utf16.includes('[') || utf16.includes('CommandLine'))
310
- return utf16;
311
- }
312
- return utf8;
313
- }
314
291
  /**
315
292
  * Cross-platform command existence check.
316
293
  */
@@ -11,10 +11,11 @@
11
11
  import fs from 'fs';
12
12
  import path from 'path';
13
13
  import { spawnSync } from 'child_process';
14
- import { resolvePaths } from '../paths.js';
14
+ import { getPackageRoot, resolvePaths } from '../paths.js';
15
15
  import { isProcessRunning, killProcess, isWindows, findProcesses } from './cross-platform.js';
16
16
  import { getProcessStartTime, startTimeMatches } from './process-introspect.js';
17
17
  import { isConfirmedLeakedTestDaemon, runtimeHomeFromEnv } from './restart-safety.js';
18
+ import { decodeWindowsOutput } from './windows-output.js';
18
19
  // ── Helpers ──
19
20
  function instanceDir() {
20
21
  return resolvePaths().instanceDir;
@@ -334,8 +335,37 @@ export function removeAll(pid) {
334
335
  function killPid(pid) {
335
336
  killProcess(pid, true);
336
337
  }
338
+ function normalizeProcessPath(value) {
339
+ return value.replace(/[\\/]+/g, '/').toLowerCase();
340
+ }
341
+ function escapeRegExp(value) {
342
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
343
+ }
337
344
  /**
338
- * 扫所有 node 进程中跑 dist/index.js PID,减去当前 HOME 已登记的 main PID。
345
+ * Return whether a process command line contains an exact EvolCore package
346
+ * main entry point. The current package path is accepted verbatim, while
347
+ * other installations are accepted only when their package directory is
348
+ * explicitly named `evolcore`.
349
+ *
350
+ * Matching the package entry instead of any `dist/index.js` path prevents
351
+ * unrelated Node services (for example BrowserMCP) from being classified as
352
+ * EvolCore orphans.
353
+ */
354
+ export function isEvolCoreMainCommand(cmdline, packageRoot = getPackageRoot()) {
355
+ const mainEntry = normalizeProcessPath(path.join(packageRoot, 'dist', 'index.js'));
356
+ const command = normalizeProcessPath(cmdline);
357
+ const entryPattern = escapeRegExp(mainEntry);
358
+ if (new RegExp(`(?:^|[\\s"'=])${entryPattern}(?=$|[\\s"'])`).test(command))
359
+ return true;
360
+ // Cross-install detection (notably on Windows/macOS where process
361
+ // environments may be unavailable): only an exact `evolcore/dist/index.js`
362
+ // package suffix qualifies. This deliberately excludes BrowserMCP and
363
+ // every other package that happens to use the same entry filename.
364
+ return /(?:^|[\s"'=])(?:[^"'=]*\/)?evolcore\/dist\/index\.js(?=$|[\s"'])/.test(command);
365
+ }
366
+ /**
367
+ * 扫所有 node 进程中运行当前 EvolCore 包 dist/index.js 的 PID,减去当前 HOME
368
+ * 已登记的 main PID。
339
369
  *
340
370
  * 用途:检测跨 HOME 残留的 evolcore 主进程(例如测试套件 spawn 后未清理、
341
371
  * 旧版本 pidfile 模式遗留等),由 cmdStart/cmdRestart 在启动前提示用户。
@@ -358,7 +388,7 @@ export function findOrphanProcesses() {
358
388
  if (m.alive)
359
389
  known.add(m.record.pid);
360
390
  }
361
- // 2. 系统中所有跑 dist/index.js 的 node 进程
391
+ // 2. 先按入口文件名找候选,再用完整包路径做严格校验。
362
392
  // Use a stable filename anchor for the Windows CIM query. The full path
363
393
  // regex contains escaped separators and is applied after command lines have
364
394
  // been fetched; embedding it in the CIM pre-filter can silently miss a
@@ -371,11 +401,8 @@ export function findOrphanProcesses() {
371
401
  if (!isProcessRunning(pid))
372
402
  continue;
373
403
  const cmdline = readCmdline(pid);
374
- // 二次验证:确实是 evolcore dist/index.js
375
- if (!/dist[\\/]index\.js/.test(cmdline))
376
- continue;
377
- // 三次验证:排除内嵌 ecweb 与独立 npm 包 ec-web。
378
- if (/[\\/](?:ecweb|ec-web)[\\/]dist[\\/]index\.js/.test(cmdline))
404
+ // 二次验证:入口必须是当前 EvolCore 包的 dist/index.js
405
+ if (!isEvolCoreMainCommand(cmdline))
379
406
  continue;
380
407
  const processEnv = readProcessEnvironment(pid);
381
408
  orphans.push({
@@ -440,25 +467,6 @@ function readCmdline(pid) {
440
467
  }
441
468
  }
442
469
  }
443
- function decodeWindowsOutput(value) {
444
- if (!value)
445
- return '';
446
- const bytes = Buffer.isBuffer(value) ? value : Buffer.from(value);
447
- if (bytes.length >= 2 && bytes[0] === 0xff && bytes[1] === 0xfe)
448
- return bytes.subarray(2).toString('utf16le');
449
- if (bytes.length >= 2 && bytes[0] === 0xfe && bytes[1] === 0xff) {
450
- const swapped = Buffer.allocUnsafe(bytes.length - 2);
451
- for (let i = 2; i + 1 < bytes.length; i += 2) {
452
- swapped[i - 2] = bytes[i + 1];
453
- swapped[i - 1] = bytes[i];
454
- }
455
- return swapped.toString('utf16le');
456
- }
457
- const utf8 = bytes.toString('utf8').replace(/^\uFEFF/, '');
458
- if (utf8.includes('\u0000'))
459
- return bytes.toString('utf16le').replace(/^\uFEFF/, '');
460
- return utf8;
461
- }
462
470
  function readProcessEnvironment(pid) {
463
471
  // Linux: /proc/<pid>/environ
464
472
  if (!isWindows && process.platform !== 'darwin') {
@@ -1,6 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import { resolvePaths } from '../paths.js';
3
3
  import { LogWriter } from './log-writer.js';
4
+ import { classifyToolErrorCode } from '../core/permission/tool-error-code.js';
4
5
  let currentLevel = process.env.LOG_LEVEL || 'INFO';
5
6
  const LEVELS = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3 };
6
7
  const config = {
@@ -46,30 +47,60 @@ export function normalizeStructuredLog(data) {
46
47
  if (!data || typeof data !== 'object' || Array.isArray(data))
47
48
  return data;
48
49
  const nested = data.event && typeof data.event === 'object' ? data.event : undefined;
50
+ const eventType = data.type ?? nested?.type;
49
51
  const correlationId = data.correlationId
52
+ ?? data.correlation_id
50
53
  ?? data.callId
54
+ ?? data.call_id
55
+ ?? data.toolUseId
56
+ ?? data.tool_use_id
51
57
  ?? data.requestId
58
+ ?? data.request_id
52
59
  ?? data.operationId
53
60
  ?? data.msgId
54
61
  ?? nested?.correlationId
55
- ?? nested?.callId;
56
- const sessionId = data.sessionId ?? nested?.sessionId;
57
- const agentAid = data.agentAid ?? data.selfAid ?? nested?.agentAid;
58
- const toolName = data.toolName ?? data.tool ?? nested?.toolName ?? nested?.name;
59
- const isToolResult = data.type === 'tool:result' || nested?.type === 'tool_result';
62
+ ?? nested?.correlation_id
63
+ ?? nested?.callId
64
+ ?? nested?.call_id
65
+ ?? nested?.toolUseId
66
+ ?? nested?.tool_use_id;
67
+ const sessionId = data.sessionId ?? data.session_id ?? nested?.sessionId ?? nested?.session_id;
68
+ const agentAid = data.agentAid ?? data.agent_aid ?? data.selfAid ?? nested?.agentAid ?? nested?.agent_aid;
69
+ const permissionMode = data.permissionMode ?? data.permission_mode ?? nested?.permissionMode ?? nested?.permission_mode;
70
+ const toolName = data.toolName ?? data.tool ?? data.name ?? nested?.toolName ?? nested?.name;
71
+ const isToolResult = eventType === 'tool:result' || eventType === 'tool_result';
72
+ const isToolUse = eventType === 'tool:use' || eventType === 'tool_use';
73
+ const isError = data.isError ?? data.is_error ?? (data.ok === false ? true : undefined)
74
+ ?? nested?.isError ?? nested?.is_error ?? (nested?.ok === false ? true : undefined);
75
+ const error = data.error ?? data.errorMessage ?? nested?.error ?? nested?.errorMessage;
76
+ const result = data.result ?? data.content ?? nested?.result ?? nested?.content;
60
77
  const decision = data.decision
61
78
  ?? data.status
62
- ?? (isToolResult && (data.isError === true || nested?.isError === true) ? 'error' : undefined)
63
- ?? (isToolResult && (data.isError === false || nested?.isError === false) ? 'allow' : undefined)
64
- ?? (nested?.type === 'tool_use' ? 'started' : undefined)
79
+ ?? (isToolResult && isError === true ? 'error' : undefined)
80
+ ?? (isToolResult && isError === false ? 'allow' : undefined)
81
+ ?? (isToolUse ? 'started' : undefined)
65
82
  ?? nested?.decision;
83
+ const explicitErrorCode = data.errorCode
84
+ ?? data.error_code
85
+ ?? nested?.errorCode
86
+ ?? nested?.error_code;
87
+ const errorCode = isToolResult && isError === true
88
+ ? classifyToolErrorCode({ errorCode: explicitErrorCode, error, result })
89
+ : explicitErrorCode;
90
+ const lifecycleRecord = isToolUse || isToolResult;
91
+ const missingContextFields = lifecycleRecord
92
+ ? ['sessionId', 'agentAid', 'permissionMode'].filter(field => ({ sessionId, agentAid, permissionMode }[field] == null))
93
+ : [];
66
94
  return {
67
95
  ...data,
68
96
  ...(correlationId ? { correlationId } : {}),
69
- ...(sessionId ? { sessionId } : {}),
70
- ...(agentAid ? { agentAid } : {}),
97
+ ...(sessionId ? { sessionId } : lifecycleRecord ? { sessionId: 'unknown' } : {}),
98
+ ...(agentAid ? { agentAid } : lifecycleRecord ? { agentAid: 'unknown' } : {}),
99
+ ...(permissionMode ? { permissionMode } : lifecycleRecord ? { permissionMode: 'unknown' } : {}),
71
100
  ...(toolName ? { toolName } : {}),
72
101
  ...(decision ? { decision } : {}),
102
+ ...(errorCode ? { errorCode } : {}),
103
+ ...(missingContextFields.length > 0 ? { contextMissing: missingContextFields } : {}),
73
104
  };
74
105
  }
75
106
  export function localTimestamp() {
@@ -2,19 +2,35 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { spawnSync } from 'child_process';
4
4
  import { getPackageRoot } from '../paths.js';
5
+ import { decodeWindowsOutput } from './windows-output.js';
5
6
  export const WINDOWS_AUTOSTART_TASK_NAME = 'EvolCore';
6
7
  function isWindows() {
7
8
  return process.platform === 'win32';
8
9
  }
9
10
  function runSchtasks(args) {
10
11
  return spawnSync('schtasks.exe', args, {
11
- encoding: 'utf8',
12
+ encoding: 'buffer',
12
13
  windowsHide: true,
13
14
  timeout: 15_000,
14
15
  });
15
16
  }
16
17
  function commandError(result, fallback) {
17
- return String(result.error?.message || result.stderr || result.stdout || fallback).trim();
18
+ const output = decodeWindowsOutput(result.stderr) || decodeWindowsOutput(result.stdout);
19
+ const status = result.status == null ? '' : `(退出码 ${result.status})`;
20
+ const signal = result.signal ? `(信号 ${result.signal})` : '';
21
+ return `${result.error?.message || output || fallback}${status}${signal}`.trim();
22
+ }
23
+ function isTaskNotFoundMessage(message) {
24
+ return /cannot find the file specified|系统找不到指定的文件|找不到指定的文件|指定的任务不存在|任务不存在/i.test(message);
25
+ }
26
+ function probeWindowsTask() {
27
+ const result = runSchtasks(['/Query', '/TN', WINDOWS_AUTOSTART_TASK_NAME]);
28
+ if (result.status === 0)
29
+ return { state: 'installed' };
30
+ const message = decodeWindowsOutput(result.stderr) || decodeWindowsOutput(result.stdout);
31
+ if (isTaskNotFoundMessage(message))
32
+ return { state: 'not-found' };
33
+ return { state: 'error', error: commandError(result, '查询 Windows 登录自启任务失败') };
18
34
  }
19
35
  function powershellLiteral(value) {
20
36
  return `'${value.replace(/'/g, "''")}'`;
@@ -28,8 +44,7 @@ function wrapperPath(runtimeRoot) {
28
44
  export function windowsAutostartInstalled() {
29
45
  if (!isWindows())
30
46
  return false;
31
- const result = runSchtasks(['/Query', '/TN', WINDOWS_AUTOSTART_TASK_NAME]);
32
- return result.status === 0;
47
+ return probeWindowsTask().state === 'installed';
33
48
  }
34
49
  function createWrapper(runtimeRoot) {
35
50
  const scriptPath = wrapperPath(runtimeRoot);
@@ -73,10 +88,18 @@ export function configureWindowsAutostart(enabled, runtimeRoot) {
73
88
  const scriptPath = wrapperPath(absoluteRoot);
74
89
  if (!enabled) {
75
90
  const result = runSchtasks(['/Delete', '/TN', WINDOWS_AUTOSTART_TASK_NAME, '/F']);
76
- if (result.status !== 0 && windowsAutostartInstalled()) {
91
+ if (result.status !== 0) {
92
+ const probe = probeWindowsTask();
93
+ if (probe.state === 'not-found') {
94
+ try {
95
+ fs.rmSync(scriptPath, { force: true });
96
+ }
97
+ catch { }
98
+ return { ok: true, enabled: false };
99
+ }
77
100
  return {
78
101
  ok: false,
79
- enabled: true,
102
+ enabled: probe.state === 'installed',
80
103
  error: commandError(result, '删除 Windows 开机自启任务失败'),
81
104
  };
82
105
  }
@@ -91,7 +114,11 @@ export function configureWindowsAutostart(enabled, runtimeRoot) {
91
114
  try {
92
115
  if (fs.existsSync(scriptPath))
93
116
  previousWrapper = fs.readFileSync(scriptPath, 'utf8');
94
- previouslyInstalled = windowsAutostartInstalled();
117
+ const previousTask = probeWindowsTask();
118
+ if (previousTask.state === 'error') {
119
+ return { ok: false, enabled: false, error: previousTask.error };
120
+ }
121
+ previouslyInstalled = previousTask.state === 'installed';
95
122
  const taskScript = createWrapper(absoluteRoot);
96
123
  const systemRoot = process.env.SystemRoot || 'C:\\Windows';
97
124
  const powershell = path.join(systemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
@@ -108,14 +135,28 @@ export function configureWindowsAutostart(enabled, runtimeRoot) {
108
135
  const result = runSchtasks(args);
109
136
  if (result.status !== 0) {
110
137
  const restoreError = restoreWrapper(scriptPath, previousWrapper);
111
- const stillInstalled = windowsAutostartInstalled();
138
+ const probe = probeWindowsTask();
112
139
  const rollbackSuffix = restoreError ? `; 恢复原启动脚本失败: ${restoreError}` : '';
113
140
  return {
114
141
  ok: false,
115
- enabled: stillInstalled,
142
+ enabled: probe.state === 'installed',
116
143
  error: `${commandError(result, '创建 Windows 开机自启任务失败')}${rollbackSuffix}`,
117
144
  };
118
145
  }
146
+ const probe = probeWindowsTask();
147
+ if (probe.state !== 'installed') {
148
+ const cleanup = runSchtasks(['/Delete', '/TN', WINDOWS_AUTOSTART_TASK_NAME, '/F']);
149
+ const afterCleanup = cleanup.status === 0 ? probeWindowsTask() : probe;
150
+ const restoreError = restoreWrapper(scriptPath, previousWrapper);
151
+ const details = probe.state === 'error' ? probe.error : '创建后无法查询到任务';
152
+ const cleanupError = cleanup.status === 0 ? '' : `; 清理任务失败: ${commandError(cleanup, '未知错误')}`;
153
+ const rollbackSuffix = restoreError ? `; 恢复原启动脚本失败: ${restoreError}` : '';
154
+ return {
155
+ ok: false,
156
+ enabled: afterCleanup.state === 'installed',
157
+ error: `${details}${cleanupError}${rollbackSuffix}`,
158
+ };
159
+ }
119
160
  return { ok: true, enabled: true };
120
161
  }
121
162
  catch (error) {
@@ -0,0 +1,36 @@
1
+ import { TextDecoder } from 'util';
2
+ /** Decode output from Windows native commands regardless of the active code page. */
3
+ export function decodeWindowsOutput(value) {
4
+ if (!value)
5
+ return '';
6
+ const bytes = Buffer.isBuffer(value) ? value : Buffer.from(value);
7
+ if (bytes.length === 0)
8
+ return '';
9
+ if (bytes.length >= 2 && bytes[0] === 0xff && bytes[1] === 0xfe) {
10
+ return bytes.subarray(2).toString('utf16le').replace(/^\uFEFF/, '');
11
+ }
12
+ if (bytes.length >= 2 && bytes[0] === 0xfe && bytes[1] === 0xff) {
13
+ const swapped = Buffer.allocUnsafe(bytes.length - 2);
14
+ for (let i = 2; i + 1 < bytes.length; i += 2) {
15
+ swapped[i - 2] = bytes[i + 1];
16
+ swapped[i - 1] = bytes[i];
17
+ }
18
+ return swapped.toString('utf16le').replace(/^\uFEFF/, '');
19
+ }
20
+ // Windows PowerShell 5.1 can emit UTF-16 without a BOM when stdout is
21
+ // redirected. NUL bytes are not expected in command text, so use them as
22
+ // the signal for this representation before trying code-page decoding.
23
+ const utf8Loose = bytes.toString('utf8');
24
+ if (utf8Loose.includes('\u0000')) {
25
+ const utf16 = bytes.toString('utf16le').replace(/^\uFEFF/, '');
26
+ if (!utf16.includes('\u0000'))
27
+ return utf16;
28
+ }
29
+ try {
30
+ return new TextDecoder('utf-8', { fatal: true }).decode(bytes).replace(/^\uFEFF/, '');
31
+ }
32
+ catch {
33
+ // schtasks.exe uses the active Windows/OEM code page for redirected output.
34
+ return new TextDecoder('gb18030').decode(bytes).replace(/^\uFEFF/, '');
35
+ }
36
+ }
@@ -176,6 +176,7 @@ ec config unset groupRules.mode \
176
176
  `role` 由角色分配服务维护;`permissionMode` 仅在角色策略中维护
177
177
  - **兼容字段**:`enable_rich_content` 仍可读取旧值,但已废弃且不再产生运行时效果
178
178
  - **群规则策略**:`groupRules.mode` 仅 Agent owner 或 daemon owner 可 set/unset;其他角色可读取当前 relation 的有效值
179
+ - **源码诊断白名单**:`readonlySourceDiagnostics` 默认关闭;开启后只读会话仅可在项目源码目录执行受限、可证明只读查询,配置/证书/快照/session/锁文件仍禁止访问
179
180
  - **仅人可写**:channels / owners / admins / 凭证 / aid / enabled / projects / aun / models.allowed
180
181
  - Agent 托管环境写仅人字段被拒
181
182
 
@@ -195,6 +195,12 @@
195
195
  "x-merge": "dict",
196
196
  "description": "Agent 能力开关及能力参数"
197
197
  },
198
+ "readonlySourceDiagnostics": {
199
+ "type": "boolean",
200
+ "x-merge": "scalar",
201
+ "default": false,
202
+ "description": "允许只读会话在显式源码目录内执行受限、可证明只读的诊断查询"
203
+ },
198
204
  "observable": {
199
205
  "type": "boolean",
200
206
  "default": false,
@@ -67,7 +67,7 @@
67
67
  "enabled": { "type": "boolean", "default": true, "description": "是否启用空闲监控" },
68
68
  "timeout": { "type": "number", "default": 120, "description": "空闲超时秒数" },
69
69
  "retryAttemptTimeout": { "type": "number", "description": "API 重试尝试连续无事件时的超时秒数;未设置时继承 timeout" },
70
- "maxExecutionTime": { "type": "number", "default": 3600, "description": "单个任务总执行时限(秒),等待审批期间仍继续计时" }
70
+ "maxExecutionTime": { "type": "number", "description": "单个任务总执行时限(秒);未配置、非数字或小于等于 0 表示不限制" }
71
71
  }
72
72
  },
73
73
  "ecweb": {
@@ -77,7 +77,7 @@
77
77
  "enabled": { "type": "boolean", "default": true, "description": "是否启用空闲监控" },
78
78
  "timeout": { "type": "number", "default": 120, "description": "空闲超时秒数" },
79
79
  "retryAttemptTimeout": { "type": "number", "description": "API 重试尝试连续无事件时的超时秒数;未设置时继承 timeout" },
80
- "maxExecutionTime": { "type": "number", "default": 3600, "description": "单个任务总执行时限(秒),等待审批期间仍继续计时" }
80
+ "maxExecutionTime": { "type": "number", "description": "单个任务总执行时限(秒);未配置、非数字或小于等于 0 表示不限制" }
81
81
  }
82
82
  },
83
83
  "ecweb": {