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.
- package/CHANGELOG.md +29 -0
- package/README.md +1 -0
- package/dist/agents/claude-runner.js +119 -29
- package/dist/agents/codex-runner.js +39 -10
- package/dist/agents/ecagent-runner.js +4 -1
- package/dist/aun/msg/p2p.js +5 -5
- package/dist/channels/aun.js +73 -20
- package/dist/channels/daemon.js +15 -9
- package/dist/channels/feishu.js +6 -1
- package/dist/cli/init.js +7 -3
- package/dist/cli/task-context.js +8 -4
- package/dist/config/config-manager.js +1 -0
- package/dist/core/audit/log-integrity.js +149 -0
- package/dist/core/auth/authorization-audit.js +73 -3
- package/dist/core/command/slash-handler.js +1 -1
- package/dist/core/event-catalog.js +1 -0
- package/dist/core/message/response-engine.js +35 -21
- package/dist/core/permission/approval-gateway.js +99 -16
- package/dist/core/permission/tool-error-code.js +47 -0
- package/dist/core/permission/tool-policy.js +61 -2
- package/dist/index.js +8 -4
- package/dist/ipc.js +11 -6
- package/dist/paths.js +18 -1
- package/dist/trigger/scheduler.js +5 -4
- package/dist/utils/cross-platform.js +1 -24
- package/dist/utils/instance-registry.js +35 -27
- package/dist/utils/logger.js +41 -10
- package/dist/utils/windows-autostart.js +50 -9
- package/dist/utils/windows-output.js +36 -0
- package/kits/docs/evolcore/config.md +1 -0
- package/kits/schemas/agent-config.schema.10.json +6 -0
- package/kits/schemas/daemon.schema.1.json +1 -1
- package/kits/schemas/daemon.schema.2.json +1 -1
- package/kits/schemas/daemon.schema.3.json +1 -1
- package/kits/schemas/daemon.schema.4.json +1 -1
- 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 =
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
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
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
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
|
-
:
|
|
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
|
-
?
|
|
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
|
-
?
|
|
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
|
-
|
|
4417
|
-
|
|
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
|
-
|
|
4485
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
-
{
|
|
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:
|
|
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
|
|
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
|
-
{
|
|
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:
|
|
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
|
|
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
|
-
//
|
|
883
|
-
//
|
|
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:
|
|
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
|
-
|
|
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
|
|
230
|
+
function daemonIdleTimeoutMs(settings) {
|
|
231
231
|
const idleTimeoutSec = settings.idleMonitor?.timeout;
|
|
232
|
-
|
|
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
|
-
:
|
|
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:
|
|
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 =
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
&&
|
|
1068
|
-
|
|
1069
|
-
|
|
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)
|
|
1672
|
+
return this.daemonChannel.executionDeadlineAt?.(startedAt);
|
|
1672
1673
|
}
|
|
1673
1674
|
clearTimer(triggerId) {
|
|
1674
1675
|
const timer = this.timers.get(triggerId);
|