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.
- package/CHANGELOG.md +17 -0
- package/README.md +1 -0
- package/dist/agents/claude-runner.js +119 -29
- package/dist/agents/codex-runner.js +36 -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/cli/init.js +7 -3
- package/dist/cli/task-context.js +2 -2
- 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/event-catalog.js +1 -0
- package/dist/core/message/response-engine.js +35 -21
- 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
package/dist/channels/daemon.js
CHANGED
|
@@ -29,14 +29,17 @@ export class DaemonChannel {
|
|
|
29
29
|
const pendingId = ctx.attemptId ?? runId;
|
|
30
30
|
const messageId = pendingId;
|
|
31
31
|
const startedAt = Date.now();
|
|
32
|
-
const remainingExecutionMs = ctx.executionDeadlineAt ===
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
const remainingExecutionMs = typeof ctx.executionDeadlineAt === 'number'
|
|
33
|
+
&& Number.isFinite(ctx.executionDeadlineAt)
|
|
34
|
+
? Math.max(1, ctx.executionDeadlineAt - startedAt)
|
|
35
|
+
: this.totalExecutionMs;
|
|
35
36
|
const promise = new Promise((resolve) => {
|
|
36
37
|
const watchdogTimer = setTimeout(() => this.watchdogTimeout(pendingId), this.watchdogMs);
|
|
37
38
|
watchdogTimer.unref?.();
|
|
38
|
-
const totalExecutionTimer =
|
|
39
|
-
|
|
39
|
+
const totalExecutionTimer = remainingExecutionMs === undefined
|
|
40
|
+
? undefined
|
|
41
|
+
: setTimeout(() => this.totalExecutionTimeout(pendingId, remainingExecutionMs), remainingExecutionMs);
|
|
42
|
+
totalExecutionTimer?.unref?.();
|
|
40
43
|
this.pending.set(pendingId, {
|
|
41
44
|
resolve,
|
|
42
45
|
startedAt,
|
|
@@ -214,7 +217,9 @@ export class DaemonChannel {
|
|
|
214
217
|
return await this.sessionManager.getOrCreateSession(this.channelName, channelId, projectPath, threadId, { channelKey: this.channelKey, peerId: `trigger:${trigger.id}`, peerName: trigger.name }, trigger.name, undefined, 'private', baseagent, trigger.agentAid, this.channelName, 'system');
|
|
215
218
|
}
|
|
216
219
|
executionDeadlineAt(startedAt = Date.now()) {
|
|
217
|
-
return
|
|
220
|
+
return this.totalExecutionMs === undefined
|
|
221
|
+
? undefined
|
|
222
|
+
: startedAt + this.totalExecutionMs;
|
|
218
223
|
}
|
|
219
224
|
async getResumableConversationSession(sessionId) {
|
|
220
225
|
const session = await this.sessionManager.getSessionById(sessionId);
|
|
@@ -281,7 +286,8 @@ export class DaemonChannel {
|
|
|
281
286
|
slot.terminalClaim = { outcome, metadata };
|
|
282
287
|
if (slot.watchdogTimer)
|
|
283
288
|
clearTimeout(slot.watchdogTimer);
|
|
284
|
-
|
|
289
|
+
if (slot.totalExecutionTimer)
|
|
290
|
+
clearTimeout(slot.totalExecutionTimer);
|
|
285
291
|
if (slot.abortListener)
|
|
286
292
|
slot.abortSignal?.removeEventListener('abort', slot.abortListener);
|
|
287
293
|
return true;
|
|
@@ -347,7 +353,7 @@ export class DaemonChannel {
|
|
|
347
353
|
slot.watchdogTimer = setTimeout(() => this.watchdogTimeout(runId), this.watchdogMs);
|
|
348
354
|
slot.watchdogTimer.unref?.();
|
|
349
355
|
}
|
|
350
|
-
totalExecutionTimeout(runId, totalExecutionMs
|
|
356
|
+
totalExecutionTimeout(runId, totalExecutionMs) {
|
|
351
357
|
const slot = this.pending.get(runId);
|
|
352
358
|
if (!slot || slot.terminalClaim)
|
|
353
359
|
return;
|
|
@@ -390,7 +396,7 @@ function normalizeWatchdogMs(value) {
|
|
|
390
396
|
function normalizeTotalExecutionMs(value) {
|
|
391
397
|
return typeof value === 'number' && Number.isFinite(value) && value > 0
|
|
392
398
|
? value
|
|
393
|
-
:
|
|
399
|
+
: undefined;
|
|
394
400
|
}
|
|
395
401
|
function channelTypeFromKey(channelKey) {
|
|
396
402
|
const idx = channelKey.indexOf('#');
|
package/dist/cli/init.js
CHANGED
|
@@ -444,6 +444,7 @@ export async function initTail(options = {}) {
|
|
|
444
444
|
const daemonConfig = loadDaemonConfig();
|
|
445
445
|
let controlAidReady = !!daemonConfig.aid;
|
|
446
446
|
let autoStartReady = true;
|
|
447
|
+
const autoStartExplicit = options.autoStart !== undefined;
|
|
447
448
|
let ownerBindingInterrupted = false;
|
|
448
449
|
if (daemonConfig.aid) {
|
|
449
450
|
console.log(`✓ 控制 AID 已存在: ${daemonConfig.aid}`);
|
|
@@ -596,13 +597,16 @@ export async function initTail(options = {}) {
|
|
|
596
597
|
}
|
|
597
598
|
// ec start 会接着启动 daemon,并在其 ready 后给出可用的 Agent 创建入口。
|
|
598
599
|
if (options.invokedByStart)
|
|
599
|
-
return controlAidReady && autoStartReady;
|
|
600
|
+
return controlAidReady && (!autoStartExplicit || autoStartReady);
|
|
600
601
|
if (!controlAidReady) {
|
|
601
602
|
console.error('\n❌ EvolCore 初始化未完成:控制 AID 未配置。联网后重新运行 ec init。');
|
|
602
603
|
return false;
|
|
603
604
|
}
|
|
604
|
-
if (!autoStartReady)
|
|
605
|
-
|
|
605
|
+
if (!autoStartReady) {
|
|
606
|
+
if (autoStartExplicit)
|
|
607
|
+
return false;
|
|
608
|
+
console.log(' ⚠️ 登录自启配置未完成,可稍后运行 ec init --auto-start 重试');
|
|
609
|
+
}
|
|
606
610
|
// 初始化完成总结
|
|
607
611
|
console.log('\n✓ EvolCore 初始化完成');
|
|
608
612
|
const finalCfg = loadDaemonConfig();
|
package/dist/cli/task-context.js
CHANGED
|
@@ -142,8 +142,8 @@ function isRunnerOwnedSessionRuntimeDir(directory) {
|
|
|
142
142
|
}
|
|
143
143
|
/** Whether a task-provided runtime directory stays inside the process-managed
|
|
144
144
|
* TMPDIR and is safe to use for transient writes. */
|
|
145
|
-
export function isManagedSessionRuntimeDir(directory) {
|
|
146
|
-
const tmpDir = process.env.TMPDIR?.trim();
|
|
145
|
+
export function isManagedSessionRuntimeDir(directory, managedRoot) {
|
|
146
|
+
const tmpDir = (managedRoot ?? process.env.TMPDIR)?.trim();
|
|
147
147
|
if (!directory || !path.isAbsolute(directory) || !tmpDir || !path.isAbsolute(tmpDir))
|
|
148
148
|
return false;
|
|
149
149
|
const root = path.resolve(tmpDir);
|
|
@@ -1120,6 +1120,7 @@ export function resolveEffective(sel, opts = {}) {
|
|
|
1120
1120
|
models: config.models,
|
|
1121
1121
|
projects: config.projects,
|
|
1122
1122
|
capabilities: config.capabilities,
|
|
1123
|
+
readonlySourceDiagnostics: config.readonlySourceDiagnostics,
|
|
1123
1124
|
observable: config.observable,
|
|
1124
1125
|
extra_backup: config.extra_backup,
|
|
1125
1126
|
// Runtime configuration parameters
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
// Logger rotation is hourly/daily, but the suffix is intentionally generic so
|
|
5
|
+
// this check also covers future rotation formats without changing the audit.
|
|
6
|
+
const STRUCTURED_LOG_RE = /^(?:daemon|events|channel-in|channel-out|messages|command-audit)(?:-[^.]+)?\.log$/;
|
|
7
|
+
const MIXED_TEXT_LOG_RE = /^daemon(?:-[^.]+)?\.log$/;
|
|
8
|
+
/** Validate and de-duplicate structured event streams by lifecycle identity. */
|
|
9
|
+
export function inspectStructuredLogs(logDir, options = {}) {
|
|
10
|
+
const issues = [];
|
|
11
|
+
const records = [];
|
|
12
|
+
let files = [];
|
|
13
|
+
try {
|
|
14
|
+
files = fs.readdirSync(logDir).filter(name => STRUCTURED_LOG_RE.test(name)
|
|
15
|
+
&& (options.includeMessages || !name.startsWith('messages')));
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return { files: 0, records: 0, uniqueRecords: 0, duplicates: 0, issues };
|
|
19
|
+
}
|
|
20
|
+
for (const file of files) {
|
|
21
|
+
let lines;
|
|
22
|
+
try {
|
|
23
|
+
lines = fs.readFileSync(path.join(logDir, file), 'utf8').split('\n');
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
lines.forEach((line, index) => {
|
|
29
|
+
let raw = line.trim();
|
|
30
|
+
if (!raw)
|
|
31
|
+
return;
|
|
32
|
+
// daemon logs mix human-readable entries with structured audit lines.
|
|
33
|
+
// Ignore the former, but still validate malformed structured payloads.
|
|
34
|
+
if (MIXED_TEXT_LOG_RE.test(file)) {
|
|
35
|
+
if (raw.startsWith('[CommandAuditRecord] '))
|
|
36
|
+
raw = raw.slice('[CommandAuditRecord] '.length);
|
|
37
|
+
else if (!raw.startsWith('{'))
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
let value;
|
|
41
|
+
try {
|
|
42
|
+
const parsed = JSON.parse(raw);
|
|
43
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
44
|
+
throw new Error('not object');
|
|
45
|
+
value = parsed;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
issues.push({ file, line: index + 1, reason: 'invalid_json' });
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const key = structuredRecordKey(value);
|
|
52
|
+
records.push({ file, line: index + 1, value, key });
|
|
53
|
+
if (isLifecycleRecord(value) && !hasContextValue(lifecycleField(value, 'sessionId')))
|
|
54
|
+
issues.push({ file, line: index + 1, reason: 'missing_session', key });
|
|
55
|
+
if (isLifecycleRecord(value) && !hasContextValue(lifecycleField(value, 'agentAid')))
|
|
56
|
+
issues.push({ file, line: index + 1, reason: 'missing_agent', key });
|
|
57
|
+
if (isLifecycleRecord(value) && !hasContextValue(lifecycleField(value, 'permissionMode')))
|
|
58
|
+
issues.push({ file, line: index + 1, reason: 'missing_permission_mode', key });
|
|
59
|
+
if (isCorrelatableRecord(value) && !key)
|
|
60
|
+
issues.push({ file, line: index + 1, reason: 'missing_correlation' });
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
const seen = new Set();
|
|
64
|
+
let duplicates = 0;
|
|
65
|
+
for (const record of records) {
|
|
66
|
+
if (!record.key)
|
|
67
|
+
continue;
|
|
68
|
+
const fingerprint = `${record.key}:${stableHash(record.value)}`;
|
|
69
|
+
if (seen.has(fingerprint)) {
|
|
70
|
+
duplicates++;
|
|
71
|
+
issues.push({ file: record.file, line: record.line, reason: 'mirror_duplicate', key: record.key });
|
|
72
|
+
}
|
|
73
|
+
else
|
|
74
|
+
seen.add(fingerprint);
|
|
75
|
+
}
|
|
76
|
+
return { files: files.length, records: records.length, uniqueRecords: records.length - duplicates, duplicates, issues };
|
|
77
|
+
}
|
|
78
|
+
function structuredRecordKey(value) {
|
|
79
|
+
const nested = nestedRecord(value);
|
|
80
|
+
const correlation = firstDefined(value, nested, [
|
|
81
|
+
'correlationId', 'correlation_id', 'callId', 'call_id', 'toolUseId', 'tool_use_id',
|
|
82
|
+
'requestId', 'request_id', 'msgId', 'operationId',
|
|
83
|
+
]);
|
|
84
|
+
if (!correlation)
|
|
85
|
+
return undefined;
|
|
86
|
+
return `${canonicalEventType(value)}:${String(correlation)}`;
|
|
87
|
+
}
|
|
88
|
+
function isLifecycleRecord(value) {
|
|
89
|
+
const type = canonicalEventType(value);
|
|
90
|
+
return type === 'tool_use' || type === 'tool_result';
|
|
91
|
+
}
|
|
92
|
+
function isCorrelatableRecord(value) {
|
|
93
|
+
const nested = nestedRecord(value);
|
|
94
|
+
return isLifecycleRecord(value)
|
|
95
|
+
|| firstDefined(value, nested, ['requestId', 'request_id', 'correlationId', 'correlation_id']) !== undefined;
|
|
96
|
+
}
|
|
97
|
+
function stableHash(value) {
|
|
98
|
+
const nested = nestedRecord(value);
|
|
99
|
+
const canonical = {
|
|
100
|
+
type: canonicalEventType(value),
|
|
101
|
+
toolName: firstDefined(value, nested, ['toolName', 'tool', 'name']),
|
|
102
|
+
callId: firstDefined(value, nested, ['callId', 'call_id', 'toolUseId', 'tool_use_id']),
|
|
103
|
+
correlationId: firstDefined(value, nested, ['correlationId', 'correlation_id']),
|
|
104
|
+
requestId: firstDefined(value, nested, ['requestId', 'request_id']),
|
|
105
|
+
sessionId: firstDefined(value, nested, ['sessionId', 'session_id']),
|
|
106
|
+
agentAid: firstDefined(value, nested, ['agentAid', 'agent_aid', 'selfAid']),
|
|
107
|
+
permissionMode: firstDefined(value, nested, ['permissionMode', 'permission_mode']),
|
|
108
|
+
isError: firstDefined(value, nested, ['isError', 'is_error']) ?? (value.ok === false || nested?.ok === false ? true : undefined),
|
|
109
|
+
errorCode: firstDefined(value, nested, ['errorCode', 'error_code']),
|
|
110
|
+
input: firstDefined(value, nested, ['input']),
|
|
111
|
+
result: firstDefined(value, nested, ['result', 'content']),
|
|
112
|
+
error: firstDefined(value, nested, ['error', 'errorMessage']),
|
|
113
|
+
};
|
|
114
|
+
return crypto.createHash('sha256').update(JSON.stringify(canonical)).digest('hex').slice(0, 16);
|
|
115
|
+
}
|
|
116
|
+
function lifecycleField(value, field) {
|
|
117
|
+
const nested = nestedRecord(value);
|
|
118
|
+
if (field === 'sessionId')
|
|
119
|
+
return firstDefined(value, nested, ['sessionId', 'session_id']);
|
|
120
|
+
if (field === 'agentAid')
|
|
121
|
+
return firstDefined(value, nested, ['agentAid', 'agent_aid', 'selfAid']);
|
|
122
|
+
return firstDefined(value, nested, ['permissionMode', 'permission_mode']);
|
|
123
|
+
}
|
|
124
|
+
function nestedRecord(value) {
|
|
125
|
+
return value.event && typeof value.event === 'object' && !Array.isArray(value.event)
|
|
126
|
+
? value.event
|
|
127
|
+
: undefined;
|
|
128
|
+
}
|
|
129
|
+
function firstDefined(value, nested, fields) {
|
|
130
|
+
for (const field of fields) {
|
|
131
|
+
if (value[field] !== undefined && value[field] !== null)
|
|
132
|
+
return value[field];
|
|
133
|
+
if (nested?.[field] !== undefined && nested[field] !== null)
|
|
134
|
+
return nested[field];
|
|
135
|
+
}
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
function canonicalEventType(value) {
|
|
139
|
+
const nested = nestedRecord(value);
|
|
140
|
+
const raw = value.type ?? nested?.type;
|
|
141
|
+
if (raw === 'tool:use' || raw === 'tool_use')
|
|
142
|
+
return 'tool_use';
|
|
143
|
+
if (raw === 'tool:result' || raw === 'tool_result')
|
|
144
|
+
return 'tool_result';
|
|
145
|
+
return String(raw ?? 'record');
|
|
146
|
+
}
|
|
147
|
+
function hasContextValue(value) {
|
|
148
|
+
return value !== undefined && value !== null && value !== '' && value !== 'unknown';
|
|
149
|
+
}
|
|
@@ -7,6 +7,7 @@ export function auditCommandAuthorization(event) {
|
|
|
7
7
|
event.decision === 'deny' ||
|
|
8
8
|
(event.decision === 'allow' && event.dangerous) ||
|
|
9
9
|
(event.source === 'agent-tool' && event.operation.startsWith('config.')) ||
|
|
10
|
+
event.operation === 'codex.approval' ||
|
|
10
11
|
event.operation.startsWith('role.') ||
|
|
11
12
|
event.operation === 'cli.exec.raw';
|
|
12
13
|
if (!shouldAudit)
|
|
@@ -30,11 +31,13 @@ export function auditToolPreflightDenial(input) {
|
|
|
30
31
|
decisionSource: 'policy',
|
|
31
32
|
toolName: input.toolName,
|
|
32
33
|
policyCode: input.policyCode,
|
|
34
|
+
protectionClass: input.protectionClass ?? protectionClassForPolicy(input.policyCode),
|
|
35
|
+
matchedPath: input.matchedPath ?? extractAuditPath(input.summary),
|
|
33
36
|
reason: input.reason,
|
|
34
37
|
argsSummary: input.summary ? { summary: input.summary } : undefined,
|
|
35
|
-
sessionId: input.sessionId,
|
|
36
|
-
agentAid: input.agentAid,
|
|
37
|
-
permissionMode: input.permissionMode,
|
|
38
|
+
sessionId: input.sessionId ?? 'unknown',
|
|
39
|
+
agentAid: input.agentAid ?? 'unknown',
|
|
40
|
+
permissionMode: input.permissionMode ?? 'unknown',
|
|
38
41
|
channel: input.channel,
|
|
39
42
|
actorId: input.actorId,
|
|
40
43
|
role: input.role ?? 'unknown',
|
|
@@ -44,6 +47,35 @@ export function auditToolPreflightDenial(input) {
|
|
|
44
47
|
taskId: input.taskId,
|
|
45
48
|
});
|
|
46
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Emit one canonical, structured record for every Codex app-server approval
|
|
52
|
+
* decision. The app-server's request id is the lifecycle key; method and
|
|
53
|
+
* decision source make policy, user, and infrastructure denials distinct.
|
|
54
|
+
*/
|
|
55
|
+
export function auditCodexApprovalDecision(input) {
|
|
56
|
+
auditCommandAuthorization({
|
|
57
|
+
ts: Date.now(),
|
|
58
|
+
source: 'agent-tool',
|
|
59
|
+
operation: 'codex.approval',
|
|
60
|
+
scope: input.toolName === 'PermissionGrant' ? 'agent' : 'filesystem',
|
|
61
|
+
dangerous: input.toolName === 'PermissionGrant',
|
|
62
|
+
decision: input.decision,
|
|
63
|
+
decisionSource: input.decisionSource,
|
|
64
|
+
requestId: input.requestId,
|
|
65
|
+
correlationId: input.correlationId ?? input.requestId,
|
|
66
|
+
sessionId: input.sessionId ?? 'unknown',
|
|
67
|
+
agentAid: input.agentAid ?? 'unknown',
|
|
68
|
+
permissionMode: input.permissionMode ?? 'unknown',
|
|
69
|
+
toolName: input.toolName,
|
|
70
|
+
approvalMethod: input.method,
|
|
71
|
+
policyCode: input.policyCode,
|
|
72
|
+
protectionClass: protectionClassForPolicy(input.policyCode),
|
|
73
|
+
reason: input.reason,
|
|
74
|
+
role: input.role ?? 'unknown',
|
|
75
|
+
taskId: input.taskId,
|
|
76
|
+
argsSummary: { method: input.method, executed: false },
|
|
77
|
+
});
|
|
78
|
+
}
|
|
47
79
|
function buildAuditRecord(event) {
|
|
48
80
|
return {
|
|
49
81
|
ts: event.ts,
|
|
@@ -53,7 +85,10 @@ function buildAuditRecord(event) {
|
|
|
53
85
|
agentAid: redactIdentifier(event.agentAid ?? event.selfAid),
|
|
54
86
|
permissionMode: event.permissionMode,
|
|
55
87
|
toolName: event.toolName,
|
|
88
|
+
approvalMethod: event.approvalMethod,
|
|
56
89
|
policyCode: event.policyCode ?? event.code,
|
|
90
|
+
protectionClass: event.protectionClass,
|
|
91
|
+
matchedPath: event.matchedPath,
|
|
57
92
|
decisionSource: event.decisionSource,
|
|
58
93
|
source: event.source,
|
|
59
94
|
operation: event.operation,
|
|
@@ -88,6 +123,9 @@ function redactIdentifier(value) {
|
|
|
88
123
|
return crypto.createHash('sha256').update(value).digest('hex').slice(0, 12);
|
|
89
124
|
}
|
|
90
125
|
function logAuditEvent(record) {
|
|
126
|
+
// Keep one machine-readable stream independent from daemon text rotation;
|
|
127
|
+
// daily analysis can de-duplicate it against events/channel mirrors.
|
|
128
|
+
writeCommandAuditRecord(record);
|
|
91
129
|
// A role-management snapshot fans out into several successful read checks.
|
|
92
130
|
// Keep those available for verbose ECWeb diagnostics without flooding INFO.
|
|
93
131
|
const routineEcwebRoleRead = record.source === 'ecweb'
|
|
@@ -116,6 +154,8 @@ function logAuditEvent(record) {
|
|
|
116
154
|
record.permissionMode ? `permissionMode=${record.permissionMode}` : null,
|
|
117
155
|
record.toolName ? `tool=${record.toolName}` : null,
|
|
118
156
|
record.policyCode ? `policy=${record.policyCode}` : null,
|
|
157
|
+
record.protectionClass ? `protectionClass=${record.protectionClass}` : null,
|
|
158
|
+
record.matchedPath ? `matchedPath=${JSON.stringify(record.matchedPath)}` : null,
|
|
119
159
|
record.decisionSource ? `decisionSource=${record.decisionSource}` : null,
|
|
120
160
|
record.code ? `code=${record.code}` : null,
|
|
121
161
|
record.matchedRule ? `rule=${record.matchedRule}` : null,
|
|
@@ -151,6 +191,21 @@ export function auditRoleMutation(record) {
|
|
|
151
191
|
logger.info(`[RoleMutationAudit] ${JSON.stringify(payload)}`);
|
|
152
192
|
}
|
|
153
193
|
let roleMutationWriter;
|
|
194
|
+
let commandAuditWriter;
|
|
195
|
+
function writeCommandAuditRecord(record) {
|
|
196
|
+
try {
|
|
197
|
+
const logDir = resolvePaths().logs;
|
|
198
|
+
if (commandAuditWriter?.logDir !== logDir) {
|
|
199
|
+
commandAuditWriter?.writer.close();
|
|
200
|
+
commandAuditWriter = {
|
|
201
|
+
logDir,
|
|
202
|
+
writer: new LogWriter({ baseName: 'command-audit', logDir, rotation: 'daily', retention: { days: 30 } }),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
commandAuditWriter.writer.write(JSON.stringify(record));
|
|
206
|
+
}
|
|
207
|
+
catch { }
|
|
208
|
+
}
|
|
154
209
|
function writeRoleMutationAudit(payload) {
|
|
155
210
|
try {
|
|
156
211
|
const logDir = resolvePaths().logs;
|
|
@@ -170,3 +225,18 @@ function writeRoleMutationAudit(payload) {
|
|
|
170
225
|
}
|
|
171
226
|
catch { }
|
|
172
227
|
}
|
|
228
|
+
function protectionClassForPolicy(policyCode) {
|
|
229
|
+
if (!policyCode)
|
|
230
|
+
return undefined;
|
|
231
|
+
if (/^h_class|^readonly_h_class|h_class_protection/i.test(policyCode))
|
|
232
|
+
return 'H';
|
|
233
|
+
if (/^l_class|^readonly_l_class|l_class_protection/i.test(policyCode))
|
|
234
|
+
return 'L';
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
237
|
+
function extractAuditPath(summary) {
|
|
238
|
+
if (!summary)
|
|
239
|
+
return undefined;
|
|
240
|
+
const match = summary.match(/(?:^|\s)(\/[^\s'"`;]+|[A-Za-z]:[\\/][^\s'"`;]+|(?:src|ecagent|ecweb|scripts|tests?)\/[^\s'"`;]+)/);
|
|
241
|
+
return match?.[1];
|
|
242
|
+
}
|
|
@@ -329,6 +329,7 @@ const CATALOG = [
|
|
|
329
329
|
{ path: 'sessionId', type: 'string' },
|
|
330
330
|
{ path: 'toolName', type: 'string' },
|
|
331
331
|
{ path: 'isError', type: 'boolean', optional: true },
|
|
332
|
+
{ path: 'errorCode', type: 'string', optional: true },
|
|
332
333
|
{ path: 'agentName', type: 'string', optional: true },
|
|
333
334
|
{ path: 'callId', type: 'string', optional: true },
|
|
334
335
|
{ path: 'correlationId', type: 'string', optional: true },
|
|
@@ -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
|
});
|
|
@@ -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
|
+
}
|