evolcore 0.0.17 → 0.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/bin/codex-managed-hook.mjs +16 -7
  3. package/bin/install-codex-managed-hooks.mjs +4 -2
  4. package/dist/agents/claude-runner.js +113 -24
  5. package/dist/agents/codex-app-server-client.js +6 -1
  6. package/dist/agents/codex-runner.js +21 -6
  7. package/dist/agents/ecagent-runner.js +39 -10
  8. package/dist/agents/gemini-runner.js +90 -19
  9. package/dist/aun/aid/store.js +36 -0
  10. package/dist/aun/msg/p2p.js +20 -8
  11. package/dist/channels/aun.js +159 -21
  12. package/dist/cli/agent-command.js +67 -6
  13. package/dist/cli/agent.js +26 -0
  14. package/dist/cli/command-log.js +23 -4
  15. package/dist/cli/daemon-commands.js +53 -12
  16. package/dist/cli/init.js +21 -5
  17. package/dist/cli/restart-monitor.js +13 -6
  18. package/dist/cli/watch-logs.js +2 -2
  19. package/dist/config/builtin-roles.js +5 -1
  20. package/dist/config/role-ranks.js +4 -0
  21. package/dist/core/audit/event-key.js +29 -0
  22. package/dist/core/audit/log-integrity.js +13 -3
  23. package/dist/core/auth/auth-gateway.js +14 -18
  24. package/dist/core/auth/authorization-audit.js +110 -3
  25. package/dist/core/auth/authorization-denial.js +17 -0
  26. package/dist/core/auth/operation-authorizer.js +143 -18
  27. package/dist/core/auth/operation-catalog.js +21 -5
  28. package/dist/core/bootstrap-messages.js +11 -6
  29. package/dist/core/bootstrap-service.js +26 -4
  30. package/dist/core/causation/aun-association.js +7 -4
  31. package/dist/core/command/agent-control.js +25 -16
  32. package/dist/core/command/command-handler.js +50 -4
  33. package/dist/core/command/group-menu.js +1 -1
  34. package/dist/core/command/menu-catalog.js +32 -7
  35. package/dist/core/command/menu-handler.js +59 -23
  36. package/dist/core/command/menu-protocol.js +196 -0
  37. package/dist/core/command/slash-gate.js +14 -5
  38. package/dist/core/command/slash-handler.js +81 -99
  39. package/dist/core/event-catalog.js +18 -0
  40. package/dist/core/message/message-bridge.js +72 -9
  41. package/dist/core/message/pause-controller.js +53 -0
  42. package/dist/core/message/response-engine.js +97 -11
  43. package/dist/core/permission/sandbox-runtime.js +79 -13
  44. package/dist/core/permission/tool-policy.js +1 -1
  45. package/dist/index.js +357 -48
  46. package/dist/ipc.js +75 -4
  47. package/dist/utils/atomic-write.js +45 -11
  48. package/dist/utils/logger.js +27 -0
  49. package/dist/utils/windows-autostart.js +740 -83
  50. package/ecagent/dist/harness/agent-harness.d.ts +1 -1
  51. package/ecagent/dist/harness/agent-harness.js +6 -4
  52. package/kits/docs/evolcore/config.md +1 -1
  53. package/kits/docs/evolcore/group-rules.md +2 -1
  54. package/kits/docs/identity/ROLE_DETAIL.md +3 -1
  55. package/kits/eck_manifest.json +25 -16
  56. package/kits/rules/01-overview.md +5 -5
  57. package/kits/rules/03-identity.md +1 -1
  58. package/kits/rules/04-relation.md +4 -4
  59. package/kits/rules/05-venue.md +5 -5
  60. package/kits/templates/bootstrap-welcome.md +3 -1
  61. package/kits/templates/system-fragments/bootstrap.md +17 -9
  62. package/package.json +1 -1
package/dist/ipc.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import net from 'net';
2
+ import { performance } from 'node:perf_hooks';
2
3
  import fs from 'fs';
3
4
  import crypto from 'crypto';
4
5
  import os from 'os';
5
6
  import path from 'path';
6
7
  import { logger } from './utils/logger.js';
8
+ import { withMenuProcessingTime, logMenuRequestCompleted, logMenuRequestReceived } from './core/command/menu-protocol.js';
7
9
  import { fileCache } from './core/daemon-file-cache.js';
8
10
  import { HANDOFF_QUERY_MAX_LIMIT, HANDOFF_STATES } from './core/handoff/types.js';
9
11
  import { AgentReloadBusyError } from './core/agent-reload-coordinator.js';
@@ -27,11 +29,14 @@ const LOCAL_CONTROL_COMMANDS = new Set([
27
29
  'wechat.contact-bind.register',
28
30
  'queue-snapshot',
29
31
  'aun-aid-stats-record-outbound',
32
+ 'evolagent.list',
33
+ 'evolagent.show',
30
34
  'evolagent.reload',
31
35
  'evolagent.load',
32
36
  'evolagent.resync',
33
37
  'menu.exec',
34
38
  'monitor-snapshot',
39
+ 'shutdown',
35
40
  ]);
36
41
  function requiresLocalControlToken(cmd) {
37
42
  // Managed bootstrap completion is authenticated by the session delegation
@@ -61,6 +66,7 @@ export class IpcServer {
61
66
  aunAidStatsProvider;
62
67
  aunAidStatsRecorder;
63
68
  menuExecutor;
69
+ agentOperationExecutor;
64
70
  statsProvider;
65
71
  agentStatsProvider;
66
72
  queueSnapshotProvider;
@@ -71,6 +77,7 @@ export class IpcServer {
71
77
  triggerExecutor;
72
78
  bootstrapCompleteExecutor;
73
79
  agentReloadExecutor;
80
+ shutdownExecutor;
74
81
  taskRuntimeContextProvider;
75
82
  codexPreToolUseExecutor;
76
83
  aunMsgSender;
@@ -121,6 +128,9 @@ export class IpcServer {
121
128
  setMenuExecutor(executor) {
122
129
  this.menuExecutor = executor;
123
130
  }
131
+ setAgentOperationExecutor(executor) {
132
+ this.agentOperationExecutor = executor;
133
+ }
124
134
  setConfigOperationExecutor(executor) {
125
135
  this.configOperationExecutor = executor;
126
136
  }
@@ -142,6 +152,10 @@ export class IpcServer {
142
152
  setAgentReloadExecutor(executor) {
143
153
  this.agentReloadExecutor = executor;
144
154
  }
155
+ /** Inject the daemon's graceful shutdown lifecycle for local stop/restart. */
156
+ setShutdownExecutor(executor) {
157
+ this.shutdownExecutor = executor;
158
+ }
145
159
  /** Inject AUN AID state aggregator for aun-aids IPC handler */
146
160
  setAunAidProvider(provider) {
147
161
  this.aunAidProvider = provider;
@@ -332,23 +346,31 @@ export class IpcServer {
332
346
  return new Promise((resolve, reject) => {
333
347
  this.server = net.createServer((conn) => {
334
348
  let buf = '';
349
+ let handled = false;
350
+ let requestReceivedAtMono;
351
+ const requestAbort = new AbortController();
335
352
  conn.on('data', async (data) => {
353
+ if (handled)
354
+ return;
355
+ requestReceivedAtMono ??= performance.now();
336
356
  buf += data.toString();
337
357
  // Simple newline-delimited JSON protocol
338
358
  const idx = buf.indexOf('\n');
339
359
  if (idx === -1)
340
360
  return;
361
+ handled = true;
341
362
  const line = buf.slice(0, idx);
342
363
  buf = buf.slice(idx + 1);
343
364
  try {
344
365
  const cmd = JSON.parse(line);
345
- const response = await this.handleCommand(cmd);
366
+ const response = await this.handleCommand(cmd, requestAbort.signal, requestReceivedAtMono);
346
367
  conn.end(JSON.stringify(response) + '\n');
347
368
  }
348
369
  catch {
349
370
  conn.end(JSON.stringify({ error: 'invalid request' }) + '\n');
350
371
  }
351
372
  });
373
+ conn.on('close', () => requestAbort.abort('Codex hook client disconnected'));
352
374
  conn.on('error', () => { }); // ignore client errors
353
375
  });
354
376
  const onListenError = (err) => {
@@ -385,7 +407,7 @@ export class IpcServer {
385
407
  catch { }
386
408
  }
387
409
  }
388
- async handleCommand(cmd) {
410
+ async handleCommand(cmd, requestSignal, receivedAtMono = performance.now()) {
389
411
  if (requiresLocalControlToken(cmd) && !this.hasValidControlToken(cmd.controlToken)) {
390
412
  return { ok: false, code: 'INVALID_CONTROL_TOKEN', error: 'valid local control token is required' };
391
413
  }
@@ -405,6 +427,7 @@ export class IpcServer {
405
427
  threadId: cmd.threadId,
406
428
  toolName: cmd.toolName,
407
429
  toolInput: cmd.toolInput,
430
+ signal: requestSignal,
408
431
  });
409
432
  }
410
433
  catch (error) {
@@ -413,6 +436,23 @@ export class IpcServer {
413
436
  }
414
437
  case 'ping':
415
438
  return { pong: true, pid: process.pid, protocolVersion: 1 };
439
+ case 'shutdown': {
440
+ if (!this.shutdownExecutor)
441
+ return { ok: false, error: 'shutdown executor not configured' };
442
+ if (Number.isInteger(cmd.expectedPid) && cmd.expectedPid !== process.pid) {
443
+ return { ok: false, code: 'PID_MISMATCH', error: 'daemon pid changed before shutdown request' };
444
+ }
445
+ const reason = typeof cmd.reason === 'string' && cmd.reason.trim()
446
+ ? cmd.reason.trim().slice(0, 80)
447
+ : 'ipc';
448
+ // Let the response flush before shutdown closes the IPC server/process.
449
+ setImmediate(() => {
450
+ Promise.resolve(this.shutdownExecutor?.(reason)).catch(error => {
451
+ logger.error(`[IPC] graceful shutdown failed: ${error instanceof Error ? error.message : String(error)}`);
452
+ });
453
+ });
454
+ return { ok: true, accepted: true, pid: process.pid };
455
+ }
416
456
  case 'bind.begin': {
417
457
  if (!this.bindExecutor)
418
458
  return { ok: false, error: 'bind executor not configured' };
@@ -833,6 +873,18 @@ export class IpcServer {
833
873
  }
834
874
  return await this.fsOperationExecutor(argv, sessionId, delegationToken, typeof cmd.delegationCommandHash === 'string' ? cmd.delegationCommandHash : undefined);
835
875
  }
876
+ case 'agent.op': {
877
+ if (!this.agentOperationExecutor)
878
+ return { ok: false, code: 'NOT_CONFIGURED', error: 'agent.op not configured' };
879
+ const { argv, sessionId, delegationToken } = cmd;
880
+ if (!Array.isArray(argv) || argv.some(value => typeof value !== 'string') || !sessionId) {
881
+ return { ok: false, code: 'INVALID_REQUEST', error: 'missing argv or sessionId' };
882
+ }
883
+ if (delegationToken !== undefined && typeof delegationToken !== 'string') {
884
+ return { ok: false, code: 'INVALID_DELEGATION', error: 'delegationToken must be a string' };
885
+ }
886
+ return await this.agentOperationExecutor(argv, sessionId, delegationToken, typeof cmd.delegationCommandHash === 'string' ? cmd.delegationCommandHash : undefined);
887
+ }
836
888
  case 'trigger.list':
837
889
  case 'trigger.show':
838
890
  case 'trigger.history':
@@ -944,17 +996,36 @@ export class IpcServer {
944
996
  }
945
997
  }
946
998
  case 'menu.exec': {
947
- if (!this.menuExecutor)
999
+ const request = cmd.payload && typeof cmd.payload === 'object' && !Array.isArray(cmd.payload)
1000
+ ? cmd.payload
1001
+ : { type: 'menu.invalid' };
1002
+ const flowContext = {
1003
+ source: 'ecweb-ipc',
1004
+ };
1005
+ logMenuRequestReceived(request, flowContext);
1006
+ if (!this.menuExecutor) {
1007
+ logMenuRequestCompleted(request, undefined, flowContext, {
1008
+ delivery: 'dropped',
1009
+ reason: 'menu-executor-not-configured',
1010
+ });
948
1011
  return { ok: false, error: 'menu.exec not configured' };
1012
+ }
949
1013
  try {
950
1014
  const response = await this.menuExecutor(cmd.payload, {
951
1015
  source: 'ecweb',
952
1016
  actorAid: null,
953
1017
  localDirect: true,
954
1018
  });
955
- return { ok: true, response };
1019
+ const timedResponse = withMenuProcessingTime(response, { receivedAtMono });
1020
+ logMenuRequestCompleted(request, timedResponse, flowContext, { delivery: 'returned' });
1021
+ return { ok: true, response: timedResponse };
956
1022
  }
957
1023
  catch (e) {
1024
+ logMenuRequestCompleted(request, undefined, flowContext, {
1025
+ delivery: 'failed',
1026
+ reason: 'handler-error',
1027
+ transportError: e,
1028
+ });
958
1029
  return { ok: false, error: e?.message ?? String(e) };
959
1030
  }
960
1031
  }
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Atomic write via dual rename.
2
+ * Atomic write with a hot backup.
3
3
  *
4
4
  * 写入流程(写 foo.json):
5
- * 1. 把当前 foo.json 改名为 foo.json_ (保留旧版作为热备)
6
- * 2. 把新内容写到 foo.json__ (写入完成才有完整内容)
5
+ * 1. 把新内容写到 foo.json__ (写入完成才有完整内容)
6
+ * 2. 保留当前 foo.json 为 foo.json_ (不移走正式文件)
7
7
  * 3. 把 foo.json__ rename 为 foo.json (原子切换)
8
8
  * foo.json_ 留到下次写入时被覆盖。
9
9
  *
@@ -17,8 +17,32 @@ import fs from 'fs';
17
17
  import path from 'path';
18
18
  const HOT = '_'; // foo.json_ 旧版热备
19
19
  const TMP = '__'; // foo.json__ 写入中
20
+ const HOT_BACKUP_FALLBACK_CODES = new Set(['EACCES', 'ENOSYS', 'ENOTSUP', 'EPERM', 'EXDEV']);
21
+ /** Preserve the previous version without making the live path disappear. */
22
+ function refreshHotBackup(filePath, hotPath) {
23
+ const backupTmp = `${hotPath}.${process.pid}.tmp`;
24
+ try {
25
+ fs.rmSync(backupTmp, { force: true });
26
+ try {
27
+ // A hard link is exact and avoids copying large protected state files.
28
+ fs.linkSync(filePath, backupTmp);
29
+ }
30
+ catch (error) {
31
+ const code = error.code;
32
+ if (!HOT_BACKUP_FALLBACK_CODES.has(code ?? ''))
33
+ throw error;
34
+ fs.copyFileSync(filePath, backupTmp);
35
+ }
36
+ // The live target remains untouched until the new file is ready.
37
+ fs.rmSync(hotPath, { force: true });
38
+ fs.renameSync(backupTmp, hotPath);
39
+ }
40
+ finally {
41
+ fs.rmSync(backupTmp, { force: true });
42
+ }
43
+ }
20
44
  /**
21
- * 原子写入 JSON 文件(双 rename)。dirname 不存在时会自动创建。
45
+ * 原子写入 JSON 文件(临时文件 + 热备)。dirname 不存在时会自动创建。
22
46
  *
23
47
  * @param filePath 目标文件
24
48
  * @param content 完整内容(不含末尾换行也可,由调用方控制)
@@ -27,15 +51,25 @@ export function atomicWrite(filePath, content) {
27
51
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
28
52
  const hot = filePath + HOT;
29
53
  const tmp = filePath + TMP;
30
- // 步骤 1:当前文件 hot(保留旧版作热备)
54
+ // Write the complete new version before changing either the live target or
55
+ // its backup. This keeps the live path present throughout the update, which
56
+ // matters to readers that project protected files into a sandbox.
57
+ fs.writeFileSync(tmp, content, 'utf-8');
58
+ // Preserve the old version while leaving the live target in place. A writer
59
+ // in another process may remove the target between existsSync and linkSync;
60
+ // in that case the new version still replaces it below and the old backup is
61
+ // simply unavailable for this write.
31
62
  if (fs.existsSync(filePath)) {
32
- if (fs.existsSync(hot))
33
- fs.unlinkSync(hot);
34
- fs.renameSync(filePath, hot);
63
+ try {
64
+ refreshHotBackup(filePath, hot);
65
+ }
66
+ catch (error) {
67
+ if (error.code !== 'ENOENT')
68
+ throw error;
69
+ }
35
70
  }
36
- // 步骤 2:写到 tmp
37
- fs.writeFileSync(tmp, content, 'utf-8');
38
- // 步骤 3:tmp → 目标(原子)
71
+ // The final rename atomically replaces the old target without an absent-path
72
+ // window.
39
73
  fs.renameSync(tmp, filePath);
40
74
  }
41
75
  export function atomicWriteJson(filePath, value) {
@@ -2,6 +2,7 @@ import fs from 'fs';
2
2
  import { resolvePaths } from '../paths.js';
3
3
  import { LogWriter } from './log-writer.js';
4
4
  import { classifyToolErrorCode } from '../core/permission/tool-error-code.js';
5
+ import { buildToolLifecycleEventKey } from '../core/audit/event-key.js';
5
6
  let currentLevel = process.env.LOG_LEVEL || 'INFO';
6
7
  const LEVELS = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3 };
7
8
  const config = {
@@ -14,6 +15,7 @@ function closeWriters(current) {
14
15
  current.main.close();
15
16
  current.message?.close();
16
17
  current.event?.close();
18
+ current.menu.close();
17
19
  current.channelIn.close();
18
20
  current.channelOut.close();
19
21
  }
@@ -32,6 +34,7 @@ function getWriters() {
32
34
  event: config.eventLog
33
35
  ? new LogWriter({ baseName: 'events', logDir, rotation: 'hourly', retention: { hours: 48 } })
34
36
  : null,
37
+ menu: new LogWriter({ baseName: 'menu-protocol', logDir, rotation: 'hourly', retention: { hours: 48 } }),
35
38
  channelIn: new LogWriter({ baseName: 'channel-in', logDir, rotation: 'hourly', retention: { hours: 48 } }),
36
39
  channelOut: new LogWriter({ baseName: 'channel-out', logDir, rotation: 'hourly', retention: { hours: 48 } }),
37
40
  };
@@ -48,6 +51,8 @@ export function normalizeStructuredLog(data) {
48
51
  return data;
49
52
  const nested = data.event && typeof data.event === 'object' ? data.event : undefined;
50
53
  const eventType = data.type ?? nested?.type;
54
+ const eventKey = data.eventKey ?? data.event_key ?? nested?.eventKey ?? nested?.event_key;
55
+ const eventPhase = data.eventPhase ?? data.event_phase ?? nested?.eventPhase ?? nested?.event_phase;
51
56
  const correlationId = data.correlationId
52
57
  ?? data.correlation_id
53
58
  ?? data.callId
@@ -80,6 +85,11 @@ export function normalizeStructuredLog(data) {
80
85
  ?? (isToolResult && isError === false ? 'allow' : undefined)
81
86
  ?? (isToolUse ? 'started' : undefined)
82
87
  ?? nested?.decision;
88
+ const executed = data.executed ?? nested?.executed
89
+ ?? (isToolResult ? true : isToolUse ? false : undefined);
90
+ const executionState = data.executionState ?? data.execution_state
91
+ ?? nested?.executionState ?? nested?.execution_state
92
+ ?? (isToolResult ? (isError === true ? 'failed' : 'completed') : isToolUse ? 'requested' : undefined);
83
93
  const explicitErrorCode = data.errorCode
84
94
  ?? data.error_code
85
95
  ?? nested?.errorCode
@@ -88,17 +98,29 @@ export function normalizeStructuredLog(data) {
88
98
  ? classifyToolErrorCode({ errorCode: explicitErrorCode, error, result })
89
99
  : explicitErrorCode;
90
100
  const lifecycleRecord = isToolUse || isToolResult;
101
+ const lifecycleEventKey = lifecycleRecord
102
+ ? (eventKey ?? buildToolLifecycleEventKey({
103
+ sessionId,
104
+ callId: data.callId ?? data.call_id ?? data.toolUseId ?? data.tool_use_id ?? nested?.callId ?? nested?.call_id,
105
+ correlationId: data.correlationId ?? data.correlation_id ?? nested?.correlationId ?? nested?.correlation_id,
106
+ requestId: data.requestId ?? data.request_id ?? nested?.requestId ?? nested?.request_id,
107
+ }))
108
+ : undefined;
91
109
  const missingContextFields = lifecycleRecord
92
110
  ? ['sessionId', 'agentAid', 'permissionMode'].filter(field => ({ sessionId, agentAid, permissionMode }[field] == null))
93
111
  : [];
94
112
  return {
95
113
  ...data,
114
+ ...(lifecycleEventKey ? { eventKey: lifecycleEventKey } : {}),
115
+ ...(eventPhase ? { eventPhase } : {}),
96
116
  ...(correlationId ? { correlationId } : {}),
97
117
  ...(sessionId ? { sessionId } : lifecycleRecord ? { sessionId: 'unknown' } : {}),
98
118
  ...(agentAid ? { agentAid } : lifecycleRecord ? { agentAid: 'unknown' } : {}),
99
119
  ...(permissionMode ? { permissionMode } : lifecycleRecord ? { permissionMode: 'unknown' } : {}),
100
120
  ...(toolName ? { toolName } : {}),
101
121
  ...(decision ? { decision } : {}),
122
+ ...(executed !== undefined ? { executed } : {}),
123
+ ...(executionState ? { executionState } : {}),
102
124
  ...(errorCode ? { errorCode } : {}),
103
125
  ...(missingContextFields.length > 0 ? { contextMissing: missingContextFields } : {}),
104
126
  };
@@ -144,6 +166,11 @@ export const logger = {
144
166
  return;
145
167
  writer.write(JSON.stringify({ ts: localTimestamp(), ...normalizeStructuredLog(data) }));
146
168
  },
169
+ menu: (data) => {
170
+ // Menu has a deliberately compact domain schema. Do not expand it with
171
+ // channel/tool lifecycle aliases used by the generic structured streams.
172
+ getWriters().menu.write(JSON.stringify({ ts: localTimestamp(), ...data }));
173
+ },
147
174
  channelIn: (data) => {
148
175
  getWriters().channelIn.write(JSON.stringify({ ts: localTimestamp(), ...normalizeStructuredLog(data) }));
149
176
  },