evolcore 0.0.14 → 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.
Files changed (36) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +1 -0
  3. package/dist/agents/claude-runner.js +119 -29
  4. package/dist/agents/codex-runner.js +39 -10
  5. package/dist/agents/ecagent-runner.js +4 -1
  6. package/dist/aun/msg/p2p.js +5 -5
  7. package/dist/channels/aun.js +73 -20
  8. package/dist/channels/daemon.js +15 -9
  9. package/dist/channels/feishu.js +6 -1
  10. package/dist/cli/init.js +7 -3
  11. package/dist/cli/task-context.js +8 -4
  12. package/dist/config/config-manager.js +1 -0
  13. package/dist/core/audit/log-integrity.js +149 -0
  14. package/dist/core/auth/authorization-audit.js +73 -3
  15. package/dist/core/command/slash-handler.js +1 -1
  16. package/dist/core/event-catalog.js +1 -0
  17. package/dist/core/message/response-engine.js +35 -21
  18. package/dist/core/permission/approval-gateway.js +99 -16
  19. package/dist/core/permission/tool-error-code.js +47 -0
  20. package/dist/core/permission/tool-policy.js +61 -2
  21. package/dist/index.js +8 -4
  22. package/dist/ipc.js +11 -6
  23. package/dist/paths.js +18 -1
  24. package/dist/trigger/scheduler.js +5 -4
  25. package/dist/utils/cross-platform.js +1 -24
  26. package/dist/utils/instance-registry.js +35 -27
  27. package/dist/utils/logger.js +41 -10
  28. package/dist/utils/windows-autostart.js +50 -9
  29. package/dist/utils/windows-output.js +36 -0
  30. package/kits/docs/evolcore/config.md +1 -0
  31. package/kits/schemas/agent-config.schema.10.json +6 -0
  32. package/kits/schemas/daemon.schema.1.json +1 -1
  33. package/kits/schemas/daemon.schema.2.json +1 -1
  34. package/kits/schemas/daemon.schema.3.json +1 -1
  35. package/kits/schemas/daemon.schema.4.json +1 -1
  36. package/package.json +1 -1
@@ -46,6 +46,7 @@ import { registerBuiltinModes } from '../../response-system/modes/index.js';
46
46
  import { deriveSessionTitle, shouldAutoFillSessionTitle } from '../session/session-title.js';
47
47
  import { SessionTurnCoordinator } from '../session/session-turn-coordinator.js';
48
48
  import { recordTriggerExecutionAnomaly } from '../../trigger/anomaly-store.js';
49
+ import { classifyToolErrorCode } from '../permission/tool-error-code.js';
49
50
  function isShowActivitiesMode(value) {
50
51
  return value === 'all' || value === 'text' || value === 'none';
51
52
  }
@@ -1388,13 +1389,15 @@ export class ResponseEngine {
1388
1389
  }
1389
1390
  }, 30000);
1390
1391
  });
1391
- const totalExecutionPromise = new Promise((_, reject) => {
1392
- totalExecutionTimer = setTimeout(() => {
1393
- logger.warn(`[ResponseEngine] Total execution timeout after ${totalExecutionMs}ms, stream: ${streamKey}`);
1394
- rejectAfterInterruptBarrier(new Error('TOTAL_EXECUTION_TIMEOUT'));
1395
- }, totalExecutionMs);
1396
- totalExecutionTimer.unref?.();
1397
- });
1392
+ const totalExecutionPromise = totalExecutionMs === undefined
1393
+ ? undefined
1394
+ : new Promise((_, reject) => {
1395
+ totalExecutionTimer = setTimeout(() => {
1396
+ logger.warn(`[ResponseEngine] Total execution timeout after ${totalExecutionMs}ms, stream: ${streamKey}`);
1397
+ rejectAfterInterruptBarrier(new Error('TOTAL_EXECUTION_TIMEOUT'));
1398
+ }, totalExecutionMs);
1399
+ totalExecutionTimer.unref?.();
1400
+ });
1398
1401
  try {
1399
1402
  const processingPromise = this._processMessageInternal(message, session, absoluteProjectPath, resetTimer, shouldSuppress, () => lastIdleSec, outputState, timeoutControl);
1400
1403
  const guardedProcessingPromise = processingPromise.then(async () => {
@@ -1403,11 +1406,10 @@ export class ResponseEngine {
1403
1406
  await timeoutControl.barrier;
1404
1407
  throw timeoutControl.error;
1405
1408
  });
1406
- await Promise.race([
1407
- guardedProcessingPromise,
1408
- timeoutPromise,
1409
- totalExecutionPromise,
1410
- ]);
1409
+ const processingPromises = [guardedProcessingPromise, timeoutPromise];
1410
+ if (totalExecutionPromise)
1411
+ processingPromises.push(totalExecutionPromise);
1412
+ await Promise.race(processingPromises);
1411
1413
  }
1412
1414
  catch (error) {
1413
1415
  if (error instanceof Error && (error.message === 'SDK_TIMEOUT' || error.message === 'TOTAL_EXECUTION_TIMEOUT')) {
@@ -1467,7 +1469,7 @@ export class ResponseEngine {
1467
1469
  && Number.isFinite(configuredSeconds)
1468
1470
  && configuredSeconds > 0
1469
1471
  ? configuredSeconds * 1000
1470
- : 60 * 60 * 1000;
1472
+ : undefined;
1471
1473
  }
1472
1474
  retryAttemptTimeoutMs() {
1473
1475
  if (this.globalSettings.idleMonitor?.enabled === false)
@@ -1807,12 +1809,14 @@ export class ResponseEngine {
1807
1809
  const statusPayload = {
1808
1810
  kind: 'status.timeout',
1809
1811
  metadata: isTotalExecutionTimeout
1810
- ? { totalExecutionMs }
1812
+ ? (totalExecutionMs === undefined ? {} : { totalExecutionMs })
1811
1813
  : { idleSec: getLastIdleSec?.() || undefined },
1812
1814
  };
1813
1815
  const idleSec = getLastIdleSec?.() || 0;
1814
1816
  const userMessage = isTotalExecutionTimeout
1815
- ? `⚠️ 任务超过总执行时限(${Math.round(totalExecutionMs / 1000)}秒),已自动中断`
1817
+ ? (totalExecutionMs === undefined
1818
+ ? '⚠️ 任务超过总执行时限,已自动中断'
1819
+ : `⚠️ 任务超过总执行时限(${Math.round(totalExecutionMs / 1000)}秒),已自动中断`)
1816
1820
  : idleSec > 0
1817
1821
  ? `⚠️ 任务超时(${idleSec}秒无响应),已自动中断`
1818
1822
  : '⚠️ 任务超时,已自动中断';
@@ -2198,6 +2202,7 @@ export class ResponseEngine {
2198
2202
  role: peerRole,
2199
2203
  chatType: authChatType,
2200
2204
  selfAid: session.selfAID || message.selfAID,
2205
+ allowReadonlySourceDiagnostics: effectiveAgentConfig?.readonlySourceDiagnostics === true,
2201
2206
  peerKey: authPeerKey,
2202
2207
  causation: taskCausation,
2203
2208
  approvalRouting,
@@ -3688,7 +3693,7 @@ export class ResponseEngine {
3688
3693
  const daemonTrigger = this.isTrustedDaemonTrigger(message);
3689
3694
  const statusPayload = procStatus === 'timeout'
3690
3695
  ? { kind: 'status.timeout', metadata: isTotalExecutionTimeout
3691
- ? { totalExecutionMs }
3696
+ ? (totalExecutionMs === undefined ? {} : { totalExecutionMs })
3692
3697
  : { idleSec: getLastIdleSec?.() || undefined } }
3693
3698
  : procStatus === 'interrupted'
3694
3699
  ? { kind: 'status.interrupted', metadata: { reason: 'stream_error' } }
@@ -3756,7 +3761,9 @@ export class ResponseEngine {
3756
3761
  : modelFallbackExhaustedMessage
3757
3762
  ? modelFallbackExhaustedMessage
3758
3763
  : isTotalExecutionTimeout
3759
- ? `⚠️ 任务超过总执行时限(${Math.round(totalExecutionMs / 1000)}秒),已自动中断`
3764
+ ? (totalExecutionMs === undefined
3765
+ ? '⚠️ 任务超过总执行时限,已自动中断'
3766
+ : `⚠️ 任务超过总执行时限(${Math.round(totalExecutionMs / 1000)}秒),已自动中断`)
3760
3767
  : isTimeout
3761
3768
  ? (idleSec > 0 ? `⚠️ 任务超时(${idleSec}秒无响应),已自动中断` : '⚠️ 任务超时,已自动中断')
3762
3769
  : getErrorMessage(error, undefined);
@@ -4097,6 +4104,12 @@ export class ResponseEngine {
4097
4104
  if (event.type === 'complete') {
4098
4105
  event = normalizeCompleteAgentEvent(event);
4099
4106
  }
4107
+ if (event.type === 'tool_result' && event.isError && !event.errorCode) {
4108
+ event = {
4109
+ ...event,
4110
+ errorCode: classifyToolErrorCode({ error: event.error, result: event.result }),
4111
+ };
4112
+ }
4100
4113
  // 每收到事件重置空闲超时
4101
4114
  const toolName = event.type === 'tool_use' ? event.name : undefined;
4102
4115
  resetTimer(event.type, toolName);
@@ -4413,8 +4426,8 @@ export class ResponseEngine {
4413
4426
  input: event.input,
4414
4427
  ...(event.callId ? { callId: event.callId } : {}),
4415
4428
  ...(event.correlationId || event.callId ? { correlationId: event.correlationId ?? event.callId } : {}),
4416
- ...(session.selfAID ? { agentAid: session.selfAID } : {}),
4417
- ...(permissionMode ? { permissionMode } : {}),
4429
+ agentAid: session.selfAID ?? 'unknown',
4430
+ permissionMode: permissionMode ?? 'unknown',
4418
4431
  timestamp: Date.now(),
4419
4432
  causation,
4420
4433
  });
@@ -4478,11 +4491,12 @@ export class ResponseEngine {
4478
4491
  sessionId: session.id,
4479
4492
  toolName: event.name,
4480
4493
  isError: event.isError,
4494
+ ...(event.isError ? { errorCode: classifyToolErrorCode({ errorCode: event.errorCode, error: event.error, result: event.result }) } : {}),
4481
4495
  agentName: agentNameForStats,
4482
4496
  ...(event.callId ? { callId: event.callId } : {}),
4483
4497
  ...(event.correlationId || event.callId ? { correlationId: event.correlationId ?? event.callId } : {}),
4484
- ...(session.selfAID ? { agentAid: session.selfAID } : {}),
4485
- ...(permissionMode ? { permissionMode } : {}),
4498
+ agentAid: session.selfAID ?? 'unknown',
4499
+ permissionMode: permissionMode ?? 'unknown',
4486
4500
  timestamp: Date.now(),
4487
4501
  causation,
4488
4502
  });
@@ -7,6 +7,7 @@ import { summarizeToolInput } from '../../utils/tool-summary.js';
7
7
  import { createRootCausation, deriveCausation, normalizeCausation } from '../causation/context.js';
8
8
  import { recordCausationSpan } from '../causation/audit.js';
9
9
  import { checkDangerousCommand } from './tool-policy.js';
10
+ import { resolveProtectedCandidate } from '../protected-paths.js';
10
11
  export async function requestDangerousCommandPermission(gateway, sessionId, toolName, input, sendPrompt, context, grantScope = 'default', mode = 'request') {
11
12
  const dangerCheck = checkDangerousCommand(toolName, input);
12
13
  if (!dangerCheck.isDangerous) {
@@ -41,6 +42,76 @@ function stablePermissionInput(value) {
41
42
  function permissionInputFingerprint(input) {
42
43
  return createHash('sha256').update(stablePermissionInput(input)).digest('hex');
43
44
  }
45
+ function collectExplicitFileChangeGrantPaths(record, output) {
46
+ const pathKeys = ['path', 'filePath', 'file_path', 'movePath', 'move_path', 'destinationPath', 'targetPath'];
47
+ const hasExplicitPath = pathKeys.some(key => typeof record[key] === 'string' && !!record[key]);
48
+ for (const key of pathKeys) {
49
+ const candidate = record[key];
50
+ if (typeof candidate === 'string' && candidate)
51
+ output.push(candidate);
52
+ }
53
+ if (record.kind && typeof record.kind === 'object' && !Array.isArray(record.kind)) {
54
+ collectExplicitFileChangeGrantPaths(record.kind, output);
55
+ }
56
+ return hasExplicitPath;
57
+ }
58
+ function collectFileChangeGrantPaths(value, output) {
59
+ if (Array.isArray(value)) {
60
+ for (const entry of value) {
61
+ if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
62
+ collectExplicitFileChangeGrantPaths(entry, output);
63
+ }
64
+ }
65
+ return;
66
+ }
67
+ if (!value || typeof value !== 'object')
68
+ return;
69
+ const record = value;
70
+ const hasExplicitPath = collectExplicitFileChangeGrantPaths(record, output);
71
+ if (hasExplicitPath)
72
+ return;
73
+ // Some backends encode file changes as a map keyed by path. Do not treat
74
+ // metadata keys as paths, and still collect explicit move destinations.
75
+ for (const [filePath, change] of Object.entries(record)) {
76
+ if (filePath !== 'kind' && filePath !== 'type')
77
+ output.push(filePath);
78
+ if (change && typeof change === 'object' && !Array.isArray(change)) {
79
+ collectExplicitFileChangeGrantPaths(change, output);
80
+ }
81
+ }
82
+ }
83
+ function permissionGrantMatch(toolName, input, options) {
84
+ const fileChangeCwd = options?.fileChangeCwd?.trim();
85
+ // grantRoot is already a broad filesystem capability. Keep it exact rather
86
+ // than silently widening it through the concrete-file convenience scope.
87
+ if (toolName === 'FileChange' && fileChangeCwd && !input.grantRoot) {
88
+ const rawPaths = [];
89
+ collectFileChangeGrantPaths(input.fileChanges, rawPaths);
90
+ try {
91
+ const canonicalPaths = [...new Set(rawPaths.map(candidate => {
92
+ const canonical = resolveProtectedCandidate(candidate, fileChangeCwd);
93
+ return process.platform === 'win32' ? canonical.toLowerCase() : canonical;
94
+ }))].sort();
95
+ if (canonicalPaths.length > 0) {
96
+ return {
97
+ inputFingerprint: permissionInputFingerprint({
98
+ scope: 'file-change-paths-v1',
99
+ paths: canonicalPaths,
100
+ }),
101
+ scope: 'file-change-paths',
102
+ };
103
+ }
104
+ }
105
+ catch {
106
+ // Invalid path input falls back to exact matching; policy checks still
107
+ // decide whether the request itself may be approved.
108
+ }
109
+ }
110
+ return {
111
+ inputFingerprint: permissionInputFingerprint(input),
112
+ scope: 'exact-operation',
113
+ };
114
+ }
44
115
  function truncateApprovalDetail(value) {
45
116
  const trimmed = value.trim();
46
117
  return trimmed.length > APPROVAL_DETAIL_LIMIT
@@ -238,13 +309,14 @@ export class PermissionGateway {
238
309
  this.temporaryGrants.delete(key);
239
310
  }
240
311
  }
241
- hasTemporaryGrant(sessionId, toolName, toolInput, grantScope = 'default') {
242
- return !!this.getTemporaryGrant(sessionId, toolName, toolInput, grantScope);
312
+ hasTemporaryGrant(sessionId, toolName, toolInput, grantScope = 'default', matchOptions) {
313
+ const match = permissionGrantMatch(toolName, toolInput, matchOptions);
314
+ return !!this.getTemporaryGrant(sessionId, toolName, match.inputFingerprint, grantScope);
243
315
  }
244
- getTemporaryGrant(sessionId, toolName, toolInput, grantScope = 'default') {
316
+ getTemporaryGrant(sessionId, toolName, inputFingerprint, grantScope = 'default') {
245
317
  const now = Date.now();
246
318
  this.pruneTemporaryGrants(now);
247
- const key = this.temporaryGrantKey(sessionId, toolName, permissionInputFingerprint(toolInput), grantScope);
319
+ const key = this.temporaryGrantKey(sessionId, toolName, inputFingerprint, grantScope);
248
320
  return this.temporaryGrants.get(key);
249
321
  }
250
322
  addTemporaryGrant(pending, causation) {
@@ -425,7 +497,7 @@ export class PermissionGateway {
425
497
  }
426
498
  return true;
427
499
  }
428
- async requestCrossSessionPermission(sessionId, challenge, route, sendPrompt, context, grantScope, requestCausation) {
500
+ async requestCrossSessionPermission(sessionId, challenge, route, sendPrompt, context, grantScope, requestCausation, grantMatch) {
429
501
  const approval = context.approvalRouting;
430
502
  const interactionRouter = context.interactionRouter;
431
503
  if (!approval || !interactionRouter) {
@@ -480,7 +552,11 @@ export class PermissionGateway {
480
552
  bodyFormat: 'markdown',
481
553
  buttons: [
482
554
  { key: 'approve_once', label: '批准本次', style: 'primary' },
483
- { key: 'approve_session_30m', label: '本会话 30 分钟', style: 'default' },
555
+ {
556
+ key: 'approve_session_30m',
557
+ label: grantMatch.scope === 'file-change-paths' ? '同文件 30 分钟' : '本会话 30 分钟',
558
+ style: 'default',
559
+ },
484
560
  { key: 'deny', label: '拒绝', style: 'danger' },
485
561
  ],
486
562
  },
@@ -508,7 +584,7 @@ export class PermissionGateway {
508
584
  displaySummary,
509
585
  reason,
510
586
  resolve,
511
- inputFingerprint: permissionInputFingerprint(toolInput),
587
+ inputFingerprint: grantMatch.inputFingerprint,
512
588
  grantScope,
513
589
  approverPolicy: challenge.approverPolicy,
514
590
  approvalRouteKind: route.kind,
@@ -644,7 +720,7 @@ export class PermissionGateway {
644
720
  /**
645
721
  * 请求人工审批。返回三态决策。
646
722
  */
647
- async requestPermission(sessionId, toolName, toolInput, sendPrompt, context, summary, reason, grantScope = 'default', approverPolicy) {
723
+ async requestPermission(sessionId, toolName, toolInput, sendPrompt, context, summary, reason, grantScope = 'default', approverPolicy, matchOptions) {
648
724
  const effectiveApproverPolicy = approverPolicy
649
725
  ?? context?.approvalRouting?.approverPolicy
650
726
  ?? 'requester';
@@ -672,7 +748,8 @@ export class PermissionGateway {
672
748
  await sendPrompt('当前操作需要授权,但无法验证申请人身份。');
673
749
  return 'deny';
674
750
  }
675
- const temporaryGrant = this.getTemporaryGrant(sessionId, toolName, toolInput, grantScope);
751
+ const grantMatch = permissionGrantMatch(toolName, toolInput, matchOptions);
752
+ const temporaryGrant = this.getTemporaryGrant(sessionId, toolName, grantMatch.inputFingerprint, grantScope);
676
753
  if (temporaryGrant) {
677
754
  const consumeCausation = deriveCausation(normalizeCausation(temporaryGrant.causation)
678
755
  ?? normalizeCausation(context?.causation)
@@ -731,7 +808,7 @@ export class PermissionGateway {
731
808
  return 'deny';
732
809
  }
733
810
  if (route.kind === 'handoff') {
734
- return this.requestCrossSessionPermission(sessionId, challenge, route, sendPrompt, context, grantScope, requestCausation);
811
+ return this.requestCrossSessionPermission(sessionId, challenge, route, sendPrompt, context, grantScope, requestCausation, grantMatch);
735
812
  }
736
813
  // 构造 ActionInteraction
737
814
  const interaction = {
@@ -743,7 +820,11 @@ export class PermissionGateway {
743
820
  body: `工具:${toolName}\n操作:${displaySummary}${reasonLine}`,
744
821
  buttons: [
745
822
  { key: 'allow', label: '✅ 允许本次', style: 'primary' },
746
- { key: 'always', label: '⏱ 同操作 30 分钟', style: 'default' },
823
+ {
824
+ key: 'always',
825
+ label: grantMatch.scope === 'file-change-paths' ? '⏱ 同文件 30 分钟' : '⏱ 同操作 30 分钟',
826
+ style: 'default',
827
+ },
747
828
  { key: 'deny', label: '❌ 拒绝', style: 'danger' },
748
829
  ],
749
830
  },
@@ -767,7 +848,7 @@ export class PermissionGateway {
767
848
  const pending = {
768
849
  sessionId,
769
850
  toolName,
770
- inputFingerprint: permissionInputFingerprint(toolInput),
851
+ inputFingerprint: grantMatch.inputFingerprint,
771
852
  grantScope,
772
853
  approverPolicy: challenge.approverPolicy,
773
854
  approvalRouteKind: route.kind,
@@ -839,7 +920,10 @@ export class PermissionGateway {
839
920
  replyContext: context.replyContext,
840
921
  causation: requestCausation,
841
922
  });
842
- const fallbackText = `🔐 权限请求 - ${toolName}\n${displaySummary}${reasonLine}\n回复 /perm ${requestId} allow 允许本次 / /perm ${requestId} always 同操作授权 30 分钟 / /perm ${requestId} deny 拒绝`;
923
+ const temporaryGrantLabel = grantMatch.scope === 'file-change-paths'
924
+ ? '同文件授权 30 分钟'
925
+ : '同操作授权 30 分钟';
926
+ const fallbackText = `🔐 权限请求 - ${toolName}\n${displaySummary}${reasonLine}\n回复 /perm ${requestId} allow 允许本次 / /perm ${requestId} always ${temporaryGrantLabel} / /perm ${requestId} deny 拒绝`;
843
927
  const result = await sendInteractionPayload(context.adapter, envelope, interaction, fallbackText, context.replyContext);
844
928
  interactionSent = !!result;
845
929
  }
@@ -879,9 +963,8 @@ export class PermissionGateway {
879
963
  const normalizedDecision = decision === 'allow' || decision === 'always'
880
964
  ? decision
881
965
  : 'deny';
882
- // Legacy "always" now means this exact operation in this session for 30 minutes.
883
- // The backend receives a one-shot allow. Future identical operations must
884
- // return through this gateway so the exact fingerprint and TTL are checked.
966
+ // The backend receives a one-shot allow. Future requests must return
967
+ // through this gateway so the selected match scope and TTL are checked.
885
968
  this.clearPendingResources(pending);
886
969
  pending.interactionRouter?.cancel(requestId);
887
970
  pending.resolve(normalizedDecision === 'deny' ? 'deny' : 'allow');
@@ -0,0 +1,47 @@
1
+ export function normalizeToolErrorCode(value) {
2
+ if (typeof value !== 'string')
3
+ return undefined;
4
+ const code = value.trim().toUpperCase().replace(/[ .-]+/g, '_');
5
+ if ([
6
+ 'POLICY_DENIED', 'ROLE_DENIED', 'USER_DENIED', 'APPROVAL_TIMEOUT',
7
+ 'DELEGATION_FAILED', 'CAPABILITY_UNAVAILABLE', 'INVALID_ARGUMENT',
8
+ 'EXECUTION_FAILED',
9
+ ].includes(code))
10
+ return code;
11
+ return undefined;
12
+ }
13
+ export function classifyToolErrorCode(input) {
14
+ const explicit = normalizeToolErrorCode(input.errorCode);
15
+ if (explicit)
16
+ return explicit;
17
+ const text = [input.error, input.result]
18
+ .map(value => {
19
+ if (typeof value === 'string')
20
+ return value;
21
+ if (value === undefined || value === null)
22
+ return '';
23
+ try {
24
+ return JSON.stringify(value);
25
+ }
26
+ catch {
27
+ return String(value);
28
+ }
29
+ })
30
+ .join(' ')
31
+ .toLowerCase();
32
+ if (/delegat(?:ion|ed)|carrier|command hash|not armed/.test(text))
33
+ return 'DELEGATION_FAILED';
34
+ if (/approval.*(?:timeout|timed out)|timed out.*approval|审批.*超时/.test(text))
35
+ return 'APPROVAL_TIMEOUT';
36
+ if (/user.*(?:denied|declined|cancel)|用户.*(?:拒绝|取消)|cancelled by user/.test(text))
37
+ return 'USER_DENIED';
38
+ if (/role|no_permission|not_allowed|visitor|member.*(?:denied|forbidden)|角色.*(?:拒绝|无权)/.test(text))
39
+ return 'ROLE_DENIED';
40
+ if (/policy|preflight|h[ .-]?class|l[ .-]?class|protected|readonly|dangerous|permission[_ ](?:denied|rejected|forbidden)|权限.*拒绝|策略.*拒绝/.test(text))
41
+ return 'POLICY_DENIED';
42
+ if (/capability|unsupported|unavailable|未找到.*工具|能力.*不可用/.test(text))
43
+ return 'CAPABILITY_UNAVAILABLE';
44
+ if (/invalid|argument|parameter|参数|用法/.test(text))
45
+ return 'INVALID_ARGUMENT';
46
+ return 'EXECUTION_FAILED';
47
+ }
@@ -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);