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
|
@@ -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);
|
|
@@ -4,6 +4,7 @@ import { execFileSync, execFile, spawn, spawnSync } from 'child_process';
|
|
|
4
4
|
import { promisify } from 'util';
|
|
5
5
|
import fs from 'fs';
|
|
6
6
|
import { getProcessStartTime, parseCimDate } from './process-introspect.js';
|
|
7
|
+
import { decodeWindowsOutput } from './windows-output.js';
|
|
7
8
|
const execFileAsync = promisify(execFile);
|
|
8
9
|
export const isWindows = process.platform === 'win32';
|
|
9
10
|
/**
|
|
@@ -287,30 +288,6 @@ function parseDateString(value) {
|
|
|
287
288
|
const parsed = Date.parse(value);
|
|
288
289
|
return Number.isNaN(parsed) ? null : parsed;
|
|
289
290
|
}
|
|
290
|
-
/** Decode PowerShell output consistently across Windows 5.1 and pwsh. */
|
|
291
|
-
function decodeWindowsOutput(value) {
|
|
292
|
-
if (!value)
|
|
293
|
-
return '';
|
|
294
|
-
const bytes = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
295
|
-
if (bytes.length >= 2 && bytes[0] === 0xff && bytes[1] === 0xfe) {
|
|
296
|
-
return bytes.subarray(2).toString('utf16le');
|
|
297
|
-
}
|
|
298
|
-
if (bytes.length >= 2 && bytes[0] === 0xfe && bytes[1] === 0xff) {
|
|
299
|
-
const swapped = Buffer.allocUnsafe(bytes.length - 2);
|
|
300
|
-
for (let i = 2; i + 1 < bytes.length; i += 2) {
|
|
301
|
-
swapped[i - 2] = bytes[i + 1];
|
|
302
|
-
swapped[i - 1] = bytes[i];
|
|
303
|
-
}
|
|
304
|
-
return swapped.toString('utf16le');
|
|
305
|
-
}
|
|
306
|
-
const utf8 = bytes.toString('utf8').replace(/^\uFEFF/, '');
|
|
307
|
-
if (utf8.includes('\u0000')) {
|
|
308
|
-
const utf16 = bytes.toString('utf16le').replace(/^\uFEFF/, '');
|
|
309
|
-
if (utf16.includes('{') || utf16.includes('[') || utf16.includes('CommandLine'))
|
|
310
|
-
return utf16;
|
|
311
|
-
}
|
|
312
|
-
return utf8;
|
|
313
|
-
}
|
|
314
291
|
/**
|
|
315
292
|
* Cross-platform command existence check.
|
|
316
293
|
*/
|
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
import fs from 'fs';
|
|
12
12
|
import path from 'path';
|
|
13
13
|
import { spawnSync } from 'child_process';
|
|
14
|
-
import { resolvePaths } from '../paths.js';
|
|
14
|
+
import { getPackageRoot, resolvePaths } from '../paths.js';
|
|
15
15
|
import { isProcessRunning, killProcess, isWindows, findProcesses } from './cross-platform.js';
|
|
16
16
|
import { getProcessStartTime, startTimeMatches } from './process-introspect.js';
|
|
17
17
|
import { isConfirmedLeakedTestDaemon, runtimeHomeFromEnv } from './restart-safety.js';
|
|
18
|
+
import { decodeWindowsOutput } from './windows-output.js';
|
|
18
19
|
// ── Helpers ──
|
|
19
20
|
function instanceDir() {
|
|
20
21
|
return resolvePaths().instanceDir;
|
|
@@ -334,8 +335,37 @@ export function removeAll(pid) {
|
|
|
334
335
|
function killPid(pid) {
|
|
335
336
|
killProcess(pid, true);
|
|
336
337
|
}
|
|
338
|
+
function normalizeProcessPath(value) {
|
|
339
|
+
return value.replace(/[\\/]+/g, '/').toLowerCase();
|
|
340
|
+
}
|
|
341
|
+
function escapeRegExp(value) {
|
|
342
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
343
|
+
}
|
|
337
344
|
/**
|
|
338
|
-
*
|
|
345
|
+
* Return whether a process command line contains an exact EvolCore package
|
|
346
|
+
* main entry point. The current package path is accepted verbatim, while
|
|
347
|
+
* other installations are accepted only when their package directory is
|
|
348
|
+
* explicitly named `evolcore`.
|
|
349
|
+
*
|
|
350
|
+
* Matching the package entry instead of any `dist/index.js` path prevents
|
|
351
|
+
* unrelated Node services (for example BrowserMCP) from being classified as
|
|
352
|
+
* EvolCore orphans.
|
|
353
|
+
*/
|
|
354
|
+
export function isEvolCoreMainCommand(cmdline, packageRoot = getPackageRoot()) {
|
|
355
|
+
const mainEntry = normalizeProcessPath(path.join(packageRoot, 'dist', 'index.js'));
|
|
356
|
+
const command = normalizeProcessPath(cmdline);
|
|
357
|
+
const entryPattern = escapeRegExp(mainEntry);
|
|
358
|
+
if (new RegExp(`(?:^|[\\s"'=])${entryPattern}(?=$|[\\s"'])`).test(command))
|
|
359
|
+
return true;
|
|
360
|
+
// Cross-install detection (notably on Windows/macOS where process
|
|
361
|
+
// environments may be unavailable): only an exact `evolcore/dist/index.js`
|
|
362
|
+
// package suffix qualifies. This deliberately excludes BrowserMCP and
|
|
363
|
+
// every other package that happens to use the same entry filename.
|
|
364
|
+
return /(?:^|[\s"'=])(?:[^"'=]*\/)?evolcore\/dist\/index\.js(?=$|[\s"'])/.test(command);
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* 扫所有 node 进程中运行当前 EvolCore 包 dist/index.js 的 PID,减去当前 HOME
|
|
368
|
+
* 已登记的 main PID。
|
|
339
369
|
*
|
|
340
370
|
* 用途:检测跨 HOME 残留的 evolcore 主进程(例如测试套件 spawn 后未清理、
|
|
341
371
|
* 旧版本 pidfile 模式遗留等),由 cmdStart/cmdRestart 在启动前提示用户。
|
|
@@ -358,7 +388,7 @@ export function findOrphanProcesses() {
|
|
|
358
388
|
if (m.alive)
|
|
359
389
|
known.add(m.record.pid);
|
|
360
390
|
}
|
|
361
|
-
// 2.
|
|
391
|
+
// 2. 先按入口文件名找候选,再用完整包路径做严格校验。
|
|
362
392
|
// Use a stable filename anchor for the Windows CIM query. The full path
|
|
363
393
|
// regex contains escaped separators and is applied after command lines have
|
|
364
394
|
// been fetched; embedding it in the CIM pre-filter can silently miss a
|
|
@@ -371,11 +401,8 @@ export function findOrphanProcesses() {
|
|
|
371
401
|
if (!isProcessRunning(pid))
|
|
372
402
|
continue;
|
|
373
403
|
const cmdline = readCmdline(pid);
|
|
374
|
-
//
|
|
375
|
-
if (
|
|
376
|
-
continue;
|
|
377
|
-
// 三次验证:排除内嵌 ecweb 与独立 npm 包 ec-web。
|
|
378
|
-
if (/[\\/](?:ecweb|ec-web)[\\/]dist[\\/]index\.js/.test(cmdline))
|
|
404
|
+
// 二次验证:入口必须是当前 EvolCore 包的 dist/index.js。
|
|
405
|
+
if (!isEvolCoreMainCommand(cmdline))
|
|
379
406
|
continue;
|
|
380
407
|
const processEnv = readProcessEnvironment(pid);
|
|
381
408
|
orphans.push({
|
|
@@ -440,25 +467,6 @@ function readCmdline(pid) {
|
|
|
440
467
|
}
|
|
441
468
|
}
|
|
442
469
|
}
|
|
443
|
-
function decodeWindowsOutput(value) {
|
|
444
|
-
if (!value)
|
|
445
|
-
return '';
|
|
446
|
-
const bytes = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
447
|
-
if (bytes.length >= 2 && bytes[0] === 0xff && bytes[1] === 0xfe)
|
|
448
|
-
return bytes.subarray(2).toString('utf16le');
|
|
449
|
-
if (bytes.length >= 2 && bytes[0] === 0xfe && bytes[1] === 0xff) {
|
|
450
|
-
const swapped = Buffer.allocUnsafe(bytes.length - 2);
|
|
451
|
-
for (let i = 2; i + 1 < bytes.length; i += 2) {
|
|
452
|
-
swapped[i - 2] = bytes[i + 1];
|
|
453
|
-
swapped[i - 1] = bytes[i];
|
|
454
|
-
}
|
|
455
|
-
return swapped.toString('utf16le');
|
|
456
|
-
}
|
|
457
|
-
const utf8 = bytes.toString('utf8').replace(/^\uFEFF/, '');
|
|
458
|
-
if (utf8.includes('\u0000'))
|
|
459
|
-
return bytes.toString('utf16le').replace(/^\uFEFF/, '');
|
|
460
|
-
return utf8;
|
|
461
|
-
}
|
|
462
470
|
function readProcessEnvironment(pid) {
|
|
463
471
|
// Linux: /proc/<pid>/environ
|
|
464
472
|
if (!isWindows && process.platform !== 'darwin') {
|
package/dist/utils/logger.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import { resolvePaths } from '../paths.js';
|
|
3
3
|
import { LogWriter } from './log-writer.js';
|
|
4
|
+
import { classifyToolErrorCode } from '../core/permission/tool-error-code.js';
|
|
4
5
|
let currentLevel = process.env.LOG_LEVEL || 'INFO';
|
|
5
6
|
const LEVELS = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3 };
|
|
6
7
|
const config = {
|
|
@@ -46,30 +47,60 @@ export function normalizeStructuredLog(data) {
|
|
|
46
47
|
if (!data || typeof data !== 'object' || Array.isArray(data))
|
|
47
48
|
return data;
|
|
48
49
|
const nested = data.event && typeof data.event === 'object' ? data.event : undefined;
|
|
50
|
+
const eventType = data.type ?? nested?.type;
|
|
49
51
|
const correlationId = data.correlationId
|
|
52
|
+
?? data.correlation_id
|
|
50
53
|
?? data.callId
|
|
54
|
+
?? data.call_id
|
|
55
|
+
?? data.toolUseId
|
|
56
|
+
?? data.tool_use_id
|
|
51
57
|
?? data.requestId
|
|
58
|
+
?? data.request_id
|
|
52
59
|
?? data.operationId
|
|
53
60
|
?? data.msgId
|
|
54
61
|
?? nested?.correlationId
|
|
55
|
-
?? nested?.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
62
|
+
?? nested?.correlation_id
|
|
63
|
+
?? nested?.callId
|
|
64
|
+
?? nested?.call_id
|
|
65
|
+
?? nested?.toolUseId
|
|
66
|
+
?? nested?.tool_use_id;
|
|
67
|
+
const sessionId = data.sessionId ?? data.session_id ?? nested?.sessionId ?? nested?.session_id;
|
|
68
|
+
const agentAid = data.agentAid ?? data.agent_aid ?? data.selfAid ?? nested?.agentAid ?? nested?.agent_aid;
|
|
69
|
+
const permissionMode = data.permissionMode ?? data.permission_mode ?? nested?.permissionMode ?? nested?.permission_mode;
|
|
70
|
+
const toolName = data.toolName ?? data.tool ?? data.name ?? nested?.toolName ?? nested?.name;
|
|
71
|
+
const isToolResult = eventType === 'tool:result' || eventType === 'tool_result';
|
|
72
|
+
const isToolUse = eventType === 'tool:use' || eventType === 'tool_use';
|
|
73
|
+
const isError = data.isError ?? data.is_error ?? (data.ok === false ? true : undefined)
|
|
74
|
+
?? nested?.isError ?? nested?.is_error ?? (nested?.ok === false ? true : undefined);
|
|
75
|
+
const error = data.error ?? data.errorMessage ?? nested?.error ?? nested?.errorMessage;
|
|
76
|
+
const result = data.result ?? data.content ?? nested?.result ?? nested?.content;
|
|
60
77
|
const decision = data.decision
|
|
61
78
|
?? data.status
|
|
62
|
-
?? (isToolResult &&
|
|
63
|
-
?? (isToolResult &&
|
|
64
|
-
?? (
|
|
79
|
+
?? (isToolResult && isError === true ? 'error' : undefined)
|
|
80
|
+
?? (isToolResult && isError === false ? 'allow' : undefined)
|
|
81
|
+
?? (isToolUse ? 'started' : undefined)
|
|
65
82
|
?? nested?.decision;
|
|
83
|
+
const explicitErrorCode = data.errorCode
|
|
84
|
+
?? data.error_code
|
|
85
|
+
?? nested?.errorCode
|
|
86
|
+
?? nested?.error_code;
|
|
87
|
+
const errorCode = isToolResult && isError === true
|
|
88
|
+
? classifyToolErrorCode({ errorCode: explicitErrorCode, error, result })
|
|
89
|
+
: explicitErrorCode;
|
|
90
|
+
const lifecycleRecord = isToolUse || isToolResult;
|
|
91
|
+
const missingContextFields = lifecycleRecord
|
|
92
|
+
? ['sessionId', 'agentAid', 'permissionMode'].filter(field => ({ sessionId, agentAid, permissionMode }[field] == null))
|
|
93
|
+
: [];
|
|
66
94
|
return {
|
|
67
95
|
...data,
|
|
68
96
|
...(correlationId ? { correlationId } : {}),
|
|
69
|
-
...(sessionId ? { sessionId } : {}),
|
|
70
|
-
...(agentAid ? { agentAid } : {}),
|
|
97
|
+
...(sessionId ? { sessionId } : lifecycleRecord ? { sessionId: 'unknown' } : {}),
|
|
98
|
+
...(agentAid ? { agentAid } : lifecycleRecord ? { agentAid: 'unknown' } : {}),
|
|
99
|
+
...(permissionMode ? { permissionMode } : lifecycleRecord ? { permissionMode: 'unknown' } : {}),
|
|
71
100
|
...(toolName ? { toolName } : {}),
|
|
72
101
|
...(decision ? { decision } : {}),
|
|
102
|
+
...(errorCode ? { errorCode } : {}),
|
|
103
|
+
...(missingContextFields.length > 0 ? { contextMissing: missingContextFields } : {}),
|
|
73
104
|
};
|
|
74
105
|
}
|
|
75
106
|
export function localTimestamp() {
|
|
@@ -2,19 +2,35 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { spawnSync } from 'child_process';
|
|
4
4
|
import { getPackageRoot } from '../paths.js';
|
|
5
|
+
import { decodeWindowsOutput } from './windows-output.js';
|
|
5
6
|
export const WINDOWS_AUTOSTART_TASK_NAME = 'EvolCore';
|
|
6
7
|
function isWindows() {
|
|
7
8
|
return process.platform === 'win32';
|
|
8
9
|
}
|
|
9
10
|
function runSchtasks(args) {
|
|
10
11
|
return spawnSync('schtasks.exe', args, {
|
|
11
|
-
encoding: '
|
|
12
|
+
encoding: 'buffer',
|
|
12
13
|
windowsHide: true,
|
|
13
14
|
timeout: 15_000,
|
|
14
15
|
});
|
|
15
16
|
}
|
|
16
17
|
function commandError(result, fallback) {
|
|
17
|
-
|
|
18
|
+
const output = decodeWindowsOutput(result.stderr) || decodeWindowsOutput(result.stdout);
|
|
19
|
+
const status = result.status == null ? '' : `(退出码 ${result.status})`;
|
|
20
|
+
const signal = result.signal ? `(信号 ${result.signal})` : '';
|
|
21
|
+
return `${result.error?.message || output || fallback}${status}${signal}`.trim();
|
|
22
|
+
}
|
|
23
|
+
function isTaskNotFoundMessage(message) {
|
|
24
|
+
return /cannot find the file specified|系统找不到指定的文件|找不到指定的文件|指定的任务不存在|任务不存在/i.test(message);
|
|
25
|
+
}
|
|
26
|
+
function probeWindowsTask() {
|
|
27
|
+
const result = runSchtasks(['/Query', '/TN', WINDOWS_AUTOSTART_TASK_NAME]);
|
|
28
|
+
if (result.status === 0)
|
|
29
|
+
return { state: 'installed' };
|
|
30
|
+
const message = decodeWindowsOutput(result.stderr) || decodeWindowsOutput(result.stdout);
|
|
31
|
+
if (isTaskNotFoundMessage(message))
|
|
32
|
+
return { state: 'not-found' };
|
|
33
|
+
return { state: 'error', error: commandError(result, '查询 Windows 登录自启任务失败') };
|
|
18
34
|
}
|
|
19
35
|
function powershellLiteral(value) {
|
|
20
36
|
return `'${value.replace(/'/g, "''")}'`;
|
|
@@ -28,8 +44,7 @@ function wrapperPath(runtimeRoot) {
|
|
|
28
44
|
export function windowsAutostartInstalled() {
|
|
29
45
|
if (!isWindows())
|
|
30
46
|
return false;
|
|
31
|
-
|
|
32
|
-
return result.status === 0;
|
|
47
|
+
return probeWindowsTask().state === 'installed';
|
|
33
48
|
}
|
|
34
49
|
function createWrapper(runtimeRoot) {
|
|
35
50
|
const scriptPath = wrapperPath(runtimeRoot);
|
|
@@ -73,10 +88,18 @@ export function configureWindowsAutostart(enabled, runtimeRoot) {
|
|
|
73
88
|
const scriptPath = wrapperPath(absoluteRoot);
|
|
74
89
|
if (!enabled) {
|
|
75
90
|
const result = runSchtasks(['/Delete', '/TN', WINDOWS_AUTOSTART_TASK_NAME, '/F']);
|
|
76
|
-
if (result.status !== 0
|
|
91
|
+
if (result.status !== 0) {
|
|
92
|
+
const probe = probeWindowsTask();
|
|
93
|
+
if (probe.state === 'not-found') {
|
|
94
|
+
try {
|
|
95
|
+
fs.rmSync(scriptPath, { force: true });
|
|
96
|
+
}
|
|
97
|
+
catch { }
|
|
98
|
+
return { ok: true, enabled: false };
|
|
99
|
+
}
|
|
77
100
|
return {
|
|
78
101
|
ok: false,
|
|
79
|
-
enabled:
|
|
102
|
+
enabled: probe.state === 'installed',
|
|
80
103
|
error: commandError(result, '删除 Windows 开机自启任务失败'),
|
|
81
104
|
};
|
|
82
105
|
}
|
|
@@ -91,7 +114,11 @@ export function configureWindowsAutostart(enabled, runtimeRoot) {
|
|
|
91
114
|
try {
|
|
92
115
|
if (fs.existsSync(scriptPath))
|
|
93
116
|
previousWrapper = fs.readFileSync(scriptPath, 'utf8');
|
|
94
|
-
|
|
117
|
+
const previousTask = probeWindowsTask();
|
|
118
|
+
if (previousTask.state === 'error') {
|
|
119
|
+
return { ok: false, enabled: false, error: previousTask.error };
|
|
120
|
+
}
|
|
121
|
+
previouslyInstalled = previousTask.state === 'installed';
|
|
95
122
|
const taskScript = createWrapper(absoluteRoot);
|
|
96
123
|
const systemRoot = process.env.SystemRoot || 'C:\\Windows';
|
|
97
124
|
const powershell = path.join(systemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
|
|
@@ -108,14 +135,28 @@ export function configureWindowsAutostart(enabled, runtimeRoot) {
|
|
|
108
135
|
const result = runSchtasks(args);
|
|
109
136
|
if (result.status !== 0) {
|
|
110
137
|
const restoreError = restoreWrapper(scriptPath, previousWrapper);
|
|
111
|
-
const
|
|
138
|
+
const probe = probeWindowsTask();
|
|
112
139
|
const rollbackSuffix = restoreError ? `; 恢复原启动脚本失败: ${restoreError}` : '';
|
|
113
140
|
return {
|
|
114
141
|
ok: false,
|
|
115
|
-
enabled:
|
|
142
|
+
enabled: probe.state === 'installed',
|
|
116
143
|
error: `${commandError(result, '创建 Windows 开机自启任务失败')}${rollbackSuffix}`,
|
|
117
144
|
};
|
|
118
145
|
}
|
|
146
|
+
const probe = probeWindowsTask();
|
|
147
|
+
if (probe.state !== 'installed') {
|
|
148
|
+
const cleanup = runSchtasks(['/Delete', '/TN', WINDOWS_AUTOSTART_TASK_NAME, '/F']);
|
|
149
|
+
const afterCleanup = cleanup.status === 0 ? probeWindowsTask() : probe;
|
|
150
|
+
const restoreError = restoreWrapper(scriptPath, previousWrapper);
|
|
151
|
+
const details = probe.state === 'error' ? probe.error : '创建后无法查询到任务';
|
|
152
|
+
const cleanupError = cleanup.status === 0 ? '' : `; 清理任务失败: ${commandError(cleanup, '未知错误')}`;
|
|
153
|
+
const rollbackSuffix = restoreError ? `; 恢复原启动脚本失败: ${restoreError}` : '';
|
|
154
|
+
return {
|
|
155
|
+
ok: false,
|
|
156
|
+
enabled: afterCleanup.state === 'installed',
|
|
157
|
+
error: `${details}${cleanupError}${rollbackSuffix}`,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
119
160
|
return { ok: true, enabled: true };
|
|
120
161
|
}
|
|
121
162
|
catch (error) {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { TextDecoder } from 'util';
|
|
2
|
+
/** Decode output from Windows native commands regardless of the active code page. */
|
|
3
|
+
export function decodeWindowsOutput(value) {
|
|
4
|
+
if (!value)
|
|
5
|
+
return '';
|
|
6
|
+
const bytes = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
7
|
+
if (bytes.length === 0)
|
|
8
|
+
return '';
|
|
9
|
+
if (bytes.length >= 2 && bytes[0] === 0xff && bytes[1] === 0xfe) {
|
|
10
|
+
return bytes.subarray(2).toString('utf16le').replace(/^\uFEFF/, '');
|
|
11
|
+
}
|
|
12
|
+
if (bytes.length >= 2 && bytes[0] === 0xfe && bytes[1] === 0xff) {
|
|
13
|
+
const swapped = Buffer.allocUnsafe(bytes.length - 2);
|
|
14
|
+
for (let i = 2; i + 1 < bytes.length; i += 2) {
|
|
15
|
+
swapped[i - 2] = bytes[i + 1];
|
|
16
|
+
swapped[i - 1] = bytes[i];
|
|
17
|
+
}
|
|
18
|
+
return swapped.toString('utf16le').replace(/^\uFEFF/, '');
|
|
19
|
+
}
|
|
20
|
+
// Windows PowerShell 5.1 can emit UTF-16 without a BOM when stdout is
|
|
21
|
+
// redirected. NUL bytes are not expected in command text, so use them as
|
|
22
|
+
// the signal for this representation before trying code-page decoding.
|
|
23
|
+
const utf8Loose = bytes.toString('utf8');
|
|
24
|
+
if (utf8Loose.includes('\u0000')) {
|
|
25
|
+
const utf16 = bytes.toString('utf16le').replace(/^\uFEFF/, '');
|
|
26
|
+
if (!utf16.includes('\u0000'))
|
|
27
|
+
return utf16;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(bytes).replace(/^\uFEFF/, '');
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// schtasks.exe uses the active Windows/OEM code page for redirected output.
|
|
34
|
+
return new TextDecoder('gb18030').decode(bytes).replace(/^\uFEFF/, '');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -176,6 +176,7 @@ ec config unset groupRules.mode \
|
|
|
176
176
|
`role` 由角色分配服务维护;`permissionMode` 仅在角色策略中维护
|
|
177
177
|
- **兼容字段**:`enable_rich_content` 仍可读取旧值,但已废弃且不再产生运行时效果
|
|
178
178
|
- **群规则策略**:`groupRules.mode` 仅 Agent owner 或 daemon owner 可 set/unset;其他角色可读取当前 relation 的有效值
|
|
179
|
+
- **源码诊断白名单**:`readonlySourceDiagnostics` 默认关闭;开启后只读会话仅可在项目源码目录执行受限、可证明只读查询,配置/证书/快照/session/锁文件仍禁止访问
|
|
179
180
|
- **仅人可写**:channels / owners / admins / 凭证 / aid / enabled / projects / aun / models.allowed
|
|
180
181
|
- Agent 托管环境写仅人字段被拒
|
|
181
182
|
|
|
@@ -195,6 +195,12 @@
|
|
|
195
195
|
"x-merge": "dict",
|
|
196
196
|
"description": "Agent 能力开关及能力参数"
|
|
197
197
|
},
|
|
198
|
+
"readonlySourceDiagnostics": {
|
|
199
|
+
"type": "boolean",
|
|
200
|
+
"x-merge": "scalar",
|
|
201
|
+
"default": false,
|
|
202
|
+
"description": "允许只读会话在显式源码目录内执行受限、可证明只读的诊断查询"
|
|
203
|
+
},
|
|
198
204
|
"observable": {
|
|
199
205
|
"type": "boolean",
|
|
200
206
|
"default": false,
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"enabled": { "type": "boolean", "default": true, "description": "是否启用空闲监控" },
|
|
68
68
|
"timeout": { "type": "number", "default": 120, "description": "空闲超时秒数" },
|
|
69
69
|
"retryAttemptTimeout": { "type": "number", "description": "API 重试尝试连续无事件时的超时秒数;未设置时继承 timeout" },
|
|
70
|
-
"maxExecutionTime": { "type": "number", "
|
|
70
|
+
"maxExecutionTime": { "type": "number", "description": "单个任务总执行时限(秒);未配置、非数字或小于等于 0 表示不限制" }
|
|
71
71
|
}
|
|
72
72
|
},
|
|
73
73
|
"ecweb": {
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"enabled": { "type": "boolean", "default": true, "description": "是否启用空闲监控" },
|
|
78
78
|
"timeout": { "type": "number", "default": 120, "description": "空闲超时秒数" },
|
|
79
79
|
"retryAttemptTimeout": { "type": "number", "description": "API 重试尝试连续无事件时的超时秒数;未设置时继承 timeout" },
|
|
80
|
-
"maxExecutionTime": { "type": "number", "
|
|
80
|
+
"maxExecutionTime": { "type": "number", "description": "单个任务总执行时限(秒);未配置、非数字或小于等于 0 表示不限制" }
|
|
81
81
|
}
|
|
82
82
|
},
|
|
83
83
|
"ecweb": {
|