evolcore 0.0.20 → 0.0.21

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 (123) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +58 -9
  3. package/dist/agents/baseagent.js +10 -6
  4. package/dist/agents/claude-runner.js +379 -108
  5. package/dist/agents/codex-app-server-client.js +10 -2
  6. package/dist/agents/codex-runner.js +402 -135
  7. package/dist/agents/ecagent-runner.js +171 -61
  8. package/dist/agents/gemini-runner.js +130 -30
  9. package/dist/agents/request-identity.js +25 -0
  10. package/dist/agents/runner-types.js +19 -0
  11. package/dist/aun/aid/agentmd.js +59 -2
  12. package/dist/aun/aid/identity.js +4 -1
  13. package/dist/aun/aid/index.js +1 -1
  14. package/dist/aun/msg/group.js +72 -6
  15. package/dist/aun/msg/history.js +213 -36
  16. package/dist/aun/msg/managed-operation.js +58 -9
  17. package/dist/aun/msg/p2p.js +5 -0
  18. package/dist/aun/outbox.js +182 -80
  19. package/dist/aun/service-proxy.js +43 -25
  20. package/dist/channels/aun.js +409 -88
  21. package/dist/channels/daemon.js +6 -1
  22. package/dist/cli/agent-command.js +4 -3
  23. package/dist/cli/agent.js +66 -56
  24. package/dist/cli/aun-commands.js +177 -42
  25. package/dist/cli/command-log.js +10 -11
  26. package/dist/cli/contact.js +1 -0
  27. package/dist/cli/daemon-commands.js +69 -115
  28. package/dist/cli/init.js +27 -15
  29. package/dist/cli/task-context.js +46 -0
  30. package/dist/cli/trigger-command.js +1 -1
  31. package/dist/cli/watch-logs.js +10 -3
  32. package/dist/config/builtin-roles.js +1 -0
  33. package/dist/config/config-field-policy.js +16 -5
  34. package/dist/config/config-manager.js +135 -17
  35. package/dist/config/contact-operation-service.js +32 -1
  36. package/dist/config/contact-request-service.js +44 -0
  37. package/dist/config/daemon-services.js +186 -0
  38. package/dist/config/gateway-config.js +20 -9
  39. package/dist/config/role-service.js +54 -3
  40. package/dist/config/schema-migration.js +550 -0
  41. package/dist/config-store.js +151 -9
  42. package/dist/core/agent-application-service.js +279 -0
  43. package/dist/core/audit/log-integrity.js +102 -0
  44. package/dist/core/auth/agent-delegation.js +31 -1
  45. package/dist/core/auth/auth-gateway.js +33 -4
  46. package/dist/core/auth/authorization-audit.js +150 -2
  47. package/dist/core/auth/operation-authorizer.js +41 -1
  48. package/dist/core/auth/operation-catalog.js +9 -1
  49. package/dist/core/bootstrap-service.js +6 -2
  50. package/dist/core/causation/aun-association.js +7 -4
  51. package/dist/core/command/agent-control.js +56 -16
  52. package/dist/core/command/command-handler.js +290 -44
  53. package/dist/core/command/connect-menu.js +3 -4
  54. package/dist/core/command/group-menu.js +5 -7
  55. package/dist/core/command/menu-handler.js +279 -80
  56. package/dist/core/command/role-menu.js +21 -11
  57. package/dist/core/command/slash-gate.js +85 -18
  58. package/dist/core/command/slash-handler.js +350 -32
  59. package/dist/core/event-catalog.js +5 -0
  60. package/dist/core/evolagent.js +4 -0
  61. package/dist/core/handoff/dispatcher.js +4 -0
  62. package/dist/core/handoff/runtime.js +10 -0
  63. package/dist/core/handoff/store.js +32 -9
  64. package/dist/core/inference/text-inference.js +7 -15
  65. package/dist/core/message/im-renderer.js +83 -84
  66. package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
  67. package/dist/core/message/message-bridge.js +124 -10
  68. package/dist/core/message/message-log.js +14 -7
  69. package/dist/core/message/message-queue.js +206 -16
  70. package/dist/core/message/message-utils.js +12 -5
  71. package/dist/core/message/response-engine.js +486 -68
  72. package/dist/core/message/send-receipt.js +1 -0
  73. package/dist/core/message/stream-debouncer.js +9 -2
  74. package/dist/core/model/model-catalog.js +23 -15
  75. package/dist/core/model/model-diagnostics.js +28 -10
  76. package/dist/core/permission/approval-gateway.js +180 -6
  77. package/dist/core/permission/ec-command-parser.js +410 -54
  78. package/dist/core/permission/mode.js +18 -3
  79. package/dist/core/{protected-paths.js → permission/protected-paths.js} +17 -5
  80. package/dist/core/permission/readonly-shell-query.js +263 -9
  81. package/dist/core/permission/sandbox-runtime.js +159 -1
  82. package/dist/core/permission/tool-policy.js +575 -21
  83. package/dist/core/session/session-fs-store.js +154 -5
  84. package/dist/core/session/session-manager.js +299 -30
  85. package/dist/core/session/session-renew.js +19 -12
  86. package/dist/core/session/session-turn-coordinator.js +11 -4
  87. package/dist/eck/kit-renderer.js +1 -1
  88. package/dist/index.js +253 -46
  89. package/dist/ipc.js +374 -24
  90. package/dist/paths.js +64 -7
  91. package/dist/response-system/context-builder.js +1 -7
  92. package/dist/trigger/anomaly-store.js +1 -0
  93. package/dist/trigger/feedback.js +56 -5
  94. package/dist/trigger/history.js +79 -4
  95. package/dist/trigger/legacy-session-history.js +2 -2
  96. package/dist/trigger/parser.js +3 -2
  97. package/dist/trigger/validation.js +6 -1
  98. package/dist/utils/atomic-write.js +27 -0
  99. package/dist/utils/ecweb-utils.js +16 -2
  100. package/dist/utils/error-utils.js +4 -1
  101. package/dist/utils/logger.js +21 -2
  102. package/dist/utils/process-tree-stats.js +24 -4
  103. package/dist/utils/process-tree-worker.js +31 -0
  104. package/dist/utils/project-path.js +1 -2
  105. package/kits/docs/INDEX.md +1 -1
  106. package/kits/docs/evolcore/INDEX.md +1 -1
  107. package/kits/docs/evolcore/contact.md +7 -1
  108. package/kits/docs/evolcore/msg.md +16 -0
  109. package/kits/schemas/_meta.json +4 -2
  110. package/kits/schemas/agent-config.schema.11.json +13 -0
  111. package/kits/schemas/daemon.schema.5.json +0 -1
  112. package/kits/schemas/daemon.schema.6.json +131 -0
  113. package/kits/schemas/defaults.schema.5.json +15 -3
  114. package/kits/schemas/migrations/README.md +3 -1
  115. package/kits/schemas/relation-config.schema.8.json +13 -0
  116. package/kits/schemas/role-config.schema.1.json +1 -2
  117. package/kits/schemas/single-session.schema.3.json +32 -0
  118. package/kits/templates/roles/admin.json +1 -0
  119. package/kits/templates/roles/member.json +1 -0
  120. package/kits/templates/roles/visitor.json +1 -0
  121. package/package.json +6 -3
  122. package/skills/eclink/SKILL.md +2 -0
  123. package/dist/config/aun-gateway-config.js +0 -2
package/dist/paths.js CHANGED
@@ -3,6 +3,7 @@ import path from 'path';
3
3
  import os from 'os';
4
4
  import crypto from 'crypto';
5
5
  import { DAEMON_CONFIG_FILENAME, DEFAULT_RUNTIME_HOME_DIRNAME, INSTANCE_SOCKET_FILENAME, RUNTIME_HOME_ENV, WINDOWS_PIPE_PREFIX, } from './product.js';
6
+ import { migrateLegacyAgentJsonlLogs } from './core/audit/log-integrity.js';
6
7
  const isWindows = process.platform === 'win32';
7
8
  let _root = null;
8
9
  export function resolveRoot() {
@@ -118,9 +119,6 @@ export function agentVenuesDir(aid) {
118
119
  export function agentSessionsDir(aid) {
119
120
  return path.join(agentDir(aid), 'sessions');
120
121
  }
121
- export function agentLogsDir(aid) {
122
- return path.join(agentDir(aid), 'logs');
123
- }
124
122
  export function agentDataDir(aid) {
125
123
  return path.join(agentDir(aid), 'data');
126
124
  }
@@ -133,6 +131,9 @@ export function agentHandoffDir(aid) {
133
131
  export function agentOutboxPath(aid) {
134
132
  return path.join(agentDataDir(aid), 'outbox.jsonl');
135
133
  }
134
+ export function agentActivityOutboxPath(aid) {
135
+ return path.join(agentDataDir(aid), 'activity-outbox.jsonl');
136
+ }
136
137
  /** Directory-safe channel instance key used by connector runtime state. */
137
138
  function encodePathKey(value) {
138
139
  return encodeURIComponent(value);
@@ -161,11 +162,66 @@ export function channelStatePath(channelKey, ...parts) {
161
162
  export function agentTriggersDir(aid) {
162
163
  return path.join(agentDir(aid), 'triggers');
163
164
  }
164
- function resolveInstanceSocketPath(root) {
165
- if (isWindows) {
166
- const hash = crypto.createHash('sha1').update(root).digest('hex').slice(0, 12);
167
- return `\\\\.\\pipe\\${WINDOWS_PIPE_PREFIX}-${hash}`;
165
+ function stripTrailingWindowsSeparators(value) {
166
+ const stripped = value.replace(/[\\/]+$/g, '');
167
+ // Keep a drive-root separator. `C:` is a drive-relative path on Windows.
168
+ if (/^[a-zA-Z]:$/.test(stripped) && /^[a-zA-Z]:[\\/]+$/.test(value)) {
169
+ return `${stripped}\\`;
170
+ }
171
+ return stripped;
172
+ }
173
+ function normalizeWindowsSocketRoot(root) {
174
+ // Use win32 semantics explicitly: EVOLCORE_HOME can come from Git Bash even
175
+ // when the daemon itself runs under native Windows Node.
176
+ return stripTrailingWindowsSeparators(path.win32.resolve(root)).toLowerCase();
177
+ }
178
+ function windowsSocketPathForRoot(root) {
179
+ const hash = crypto.createHash('sha1').update(root).digest('hex').slice(0, 12);
180
+ return `\\\\.\\pipe\\${WINDOWS_PIPE_PREFIX}-${hash}`;
181
+ }
182
+ /**
183
+ * Return the canonical and pre-normalization Windows daemon pipe names.
184
+ *
185
+ * The extra candidates keep a daemon started by an older EvolCore build
186
+ * reachable while it is still running after the CLI has been upgraded.
187
+ */
188
+ export function getWindowsInstanceSocketPathCandidates(root) {
189
+ const normalized = path.win32.normalize(root);
190
+ const normalizedForward = stripTrailingWindowsSeparators(normalized.replace(/\\/g, '/'));
191
+ const canonical = normalizeWindowsSocketRoot(root);
192
+ const baseVariants = [
193
+ canonical,
194
+ root,
195
+ normalized,
196
+ normalizedForward,
197
+ canonical.replace(/\\/g, '/'),
198
+ root.replace(/\\/g, '/'),
199
+ root.replace(/\//g, '\\'),
200
+ ];
201
+ const variants = new Set();
202
+ for (const variant of baseVariants) {
203
+ const stripped = stripTrailingWindowsSeparators(variant);
204
+ variants.add(variant);
205
+ variants.add(stripped);
206
+ if (stripped) {
207
+ variants.add(`${stripped}\\`);
208
+ variants.add(`${stripped}/`);
209
+ }
168
210
  }
211
+ const seen = new Set();
212
+ const candidates = [];
213
+ for (const variant of variants) {
214
+ const socket = windowsSocketPathForRoot(variant);
215
+ if (seen.has(socket))
216
+ continue;
217
+ seen.add(socket);
218
+ candidates.push(socket);
219
+ }
220
+ return candidates;
221
+ }
222
+ function resolveInstanceSocketPath(root) {
223
+ if (isWindows)
224
+ return getWindowsInstanceSocketPathCandidates(root)[0];
169
225
  const filesystemPath = path.join(root, 'data', 'instance', INSTANCE_SOCKET_FILENAME);
170
226
  // Linux/macOS Unix-domain sockets have a small sockaddr path limit
171
227
  // (typically 108 bytes on Linux). Long managed TMPDIR prefixes can make a
@@ -196,6 +252,7 @@ export function ensureDataDirs() {
196
252
  fs.mkdirSync(p.outboxDir, { recursive: true });
197
253
  fs.mkdirSync(p.eckDir, { recursive: true });
198
254
  fs.mkdirSync(aidsDir(), { recursive: true });
255
+ migrateLegacyAgentJsonlLogs(p.agentsDir, p.logs);
199
256
  migrateFromAun();
200
257
  }
201
258
  const MIGRATION_MARKER = '.migrated-from-aun';
@@ -12,7 +12,6 @@
12
12
  * (Phase 6)真正需要时接入 Runner,当前提供明确的"未实现"占位,避免空壳代码。
13
13
  */
14
14
  import path from 'path';
15
- import fs from 'fs';
16
15
  export class ResponseModeContextBuilder {
17
16
  /** (sessionId::modeId) → sessionState,跨消息保持 */
18
17
  stateCache = new Map();
@@ -22,7 +21,7 @@ export class ResponseModeContextBuilder {
22
21
  */
23
22
  build(modeId, deps) {
24
23
  const sessionState = this.getOrCreateState(deps.session.id, modeId);
25
- const dataDir = this.ensureDataDir(deps.agentDir, modeId);
24
+ const dataDir = path.join(deps.agentDir, 'response-system', modeId);
26
25
  return {
27
26
  session: deps.session,
28
27
  agentConfig: deps.agentConfig,
@@ -63,9 +62,4 @@ export class ResponseModeContextBuilder {
63
62
  }
64
63
  return state;
65
64
  }
66
- ensureDataDir(agentDir, modeId) {
67
- const dir = path.join(agentDir, 'response-system', modeId);
68
- fs.mkdirSync(dir, { recursive: true });
69
- return dir;
70
- }
71
65
  }
@@ -252,6 +252,7 @@ function anomalyClassKey(anomaly) {
252
252
  anomaly.phase,
253
253
  anomaly.toolName,
254
254
  anomaly.policyCode,
255
+ anomaly.decisionSource,
255
256
  anomaly.policy,
256
257
  anomaly.agentAid,
257
258
  anomaly.sessionId,
@@ -37,8 +37,11 @@ export class TriggerFeedbackDispatcher {
37
37
  const result = await this.dispatchDirect(input, renderedText, target);
38
38
  if (result.error)
39
39
  return { status: 'failed', reason: result.error.code, feedback, effects: result.effects, error: result.error };
40
+ const replyStatus = statusFromReply(input.reply);
40
41
  return {
41
- status: statusFromReply(input.reply),
42
+ status: result.effects.some(effect => effect.status === 'queued') && replyStatus !== 'failed'
43
+ ? 'degraded'
44
+ : replyStatus,
42
45
  reason: reasonFromReply(input.reply),
43
46
  feedback,
44
47
  effects: result.effects,
@@ -204,6 +207,7 @@ export class TriggerFeedbackDispatcher {
204
207
  originChannelType: input.trigger.origin?.channelType
205
208
  ?? (input.trigger.origin?.channelKey ? channelTypeFromKey(input.trigger.origin.channelKey) : undefined),
206
209
  originControl: input.trigger.origin?.channelKey === 'daemon',
210
+ authorizedBy: input.trigger.authorizedBy,
207
211
  },
208
212
  };
209
213
  outcome = this.waitForTriggerOutcome(input.runId, input.attemptId, delivery.signal);
@@ -379,7 +383,7 @@ export class TriggerFeedbackDispatcher {
379
383
  if (isAunTarget && !targetChatType(target)) {
380
384
  throw Object.assign(new Error('AUN trigger feedback requires an explicit delivery route'), { code: 'AUN_OUTBOUND_ROUTE_REQUIRED' });
381
385
  }
382
- await raceWithAbort(binding.adapter.send(buildEnvelope({
386
+ const receipt = await raceWithAbort(binding.adapter.send(buildEnvelope({
383
387
  taskId: `trigger:${input.runId}`,
384
388
  channel: binding.adapter.channelKey,
385
389
  channelId: target.channelId,
@@ -394,16 +398,60 @@ export class TriggerFeedbackDispatcher {
394
398
  isFinal: true,
395
399
  format: 'plain',
396
400
  }), cancellation.signal);
397
- effects.push({
401
+ const common = {
398
402
  type: 'message.send',
399
- status: 'success',
400
403
  channelKey: target.channelKey,
401
404
  channelType: channelTypeFromKey(target.channelKey),
402
405
  channelId: target.channelId,
403
406
  attempt,
404
407
  startedAt,
405
408
  finishedAt: Date.now(),
406
- });
409
+ };
410
+ if (receipt?.status === 'sent' && receipt.messageId) {
411
+ effects.push({ ...common, status: 'success', operationId: receipt.operationId, messageId: receipt.messageId });
412
+ return { effects, error: null };
413
+ }
414
+ if (receipt?.status === 'queued') {
415
+ effects.push({
416
+ ...common,
417
+ status: 'queued',
418
+ operationId: receipt.operationId,
419
+ outboxId: receipt.outboxId,
420
+ error: receipt.error,
421
+ code: receipt.code,
422
+ });
423
+ return { effects, error: null };
424
+ }
425
+ if (receipt?.status === 'failed') {
426
+ effects.push({
427
+ ...common,
428
+ status: 'failed',
429
+ operationId: receipt.operationId,
430
+ outboxId: receipt.outboxId,
431
+ error: receipt.error,
432
+ code: receipt.code,
433
+ });
434
+ return {
435
+ effects,
436
+ error: {
437
+ code: String(receipt.code ?? 'send_failed'),
438
+ message: receipt.error,
439
+ },
440
+ };
441
+ }
442
+ if (receipt?.status === 'suppressed') {
443
+ const message = `channel suppressed trigger feedback: ${receipt.reason}`;
444
+ effects.push({ ...common, status: 'skipped', error: message, code: 'SEND_SUPPRESSED' });
445
+ return { effects, error: { code: 'send_suppressed', message } };
446
+ }
447
+ if (isAunTarget) {
448
+ const message = 'AUN adapter returned no delivery receipt';
449
+ effects.push({ ...common, status: 'failed', error: message, code: 'MISSING_SEND_RECEIPT' });
450
+ return { effects, error: { code: 'missing_send_receipt', message } };
451
+ }
452
+ // Non-AUN adapters are still allowed to use the legacy void result
453
+ // until their receipt migration is complete.
454
+ effects.push({ ...common, status: 'success' });
407
455
  return { effects, error: null };
408
456
  }
409
457
  catch (err) {
@@ -527,6 +575,9 @@ export class TriggerFeedbackDispatcher {
527
575
  const status = Number(err?.status ?? err?.statusCode ?? err?.code);
528
576
  const rawCode = String(err?.code ?? err?.errorCode ?? '').toLowerCase();
529
577
  const lower = `${message} ${rawCode}`.toLowerCase();
578
+ if (rawCode === 'outbox_full') {
579
+ return { retryable: false, code: 'OUTBOX_FULL', message };
580
+ }
530
581
  if (status === 408 || status === 429 || status >= 500) {
531
582
  return { retryable: true, code: 'send_retryable_error', message };
532
583
  }
@@ -2,7 +2,11 @@ import crypto from 'crypto';
2
2
  import fs from 'fs';
3
3
  import path from 'path';
4
4
  import { isManagedSessionRuntimeDir, SESSION_RUNTIME_DIR_ENV } from '../cli/task-context.js';
5
+ import { findMessageLogOperationMessageId } from '../core/message/message-log.js';
6
+ import { chatDirPath } from '../core/session/session-fs-store.js';
7
+ import { resolvePaths } from '../paths.js';
5
8
  import { definitionRevision } from './validation.js';
9
+ import * as aunOutbox from '../aun/outbox.js';
6
10
  export class TriggerHistoryStore {
7
11
  rootDir;
8
12
  agentAid;
@@ -66,7 +70,7 @@ export class TriggerHistoryStore {
66
70
  continue;
67
71
  if (triggerId && event.triggerId !== triggerId)
68
72
  continue;
69
- out.push(event);
73
+ out.push(hydrateHistoryEvent(event));
70
74
  }
71
75
  return out;
72
76
  }
@@ -80,7 +84,7 @@ export class TriggerHistoryStore {
80
84
  if (!event || !isRunEvent(event))
81
85
  continue;
82
86
  if (event.agentAid === this.agentAid && event.triggerId === triggerId) {
83
- out.push(event.audit);
87
+ out.push(hydrateTriggerAudit(event.audit));
84
88
  }
85
89
  }
86
90
  return out;
@@ -95,7 +99,7 @@ export class TriggerHistoryStore {
95
99
  const event = parseHistoryEvent(lines[i], this.agentAid);
96
100
  if (!event || !isRunEvent(event) || !pending.has(event.triggerId))
97
101
  continue;
98
- latest.set(event.triggerId, event.audit);
102
+ latest.set(event.triggerId, hydrateTriggerAudit(event.audit));
99
103
  pending.delete(event.triggerId);
100
104
  }
101
105
  return latest;
@@ -125,7 +129,7 @@ export class TriggerHistoryStore {
125
129
  }
126
130
  if (!isRunEvent(event))
127
131
  continue;
128
- const record = event.audit;
132
+ const record = hydrateTriggerAudit(event.audit);
129
133
  if (record.status !== 'completed'
130
134
  && record.status !== 'degraded'
131
135
  && record.status !== 'noop'
@@ -310,6 +314,77 @@ function finiteNumber(value) {
310
314
  function nonNegativeInteger(value) {
311
315
  return typeof value === 'number' && Number.isInteger(value) && value >= 0 ? value : undefined;
312
316
  }
317
+ function hydrateHistoryEvent(event) {
318
+ if (!isRunEvent(event))
319
+ return event;
320
+ return { ...event, audit: hydrateTriggerAudit(event.audit) };
321
+ }
322
+ /**
323
+ * Resolve queued AUN effects against the durable outbox at read time. The
324
+ * original audit is append-only, so this projection is what lets a later
325
+ * expiry become visibly failed without pretending that a missing outbox row
326
+ * was delivered.
327
+ */
328
+ function hydrateTriggerAudit(audit) {
329
+ let changed = false;
330
+ let deliveryFailure;
331
+ const effects = audit.effects.map(effect => {
332
+ if (effect.status !== 'queued' || !effect.outboxId)
333
+ return effect;
334
+ const state = aunOutbox.deliveryState(audit.agentAid, effect.outboxId);
335
+ if (state.status === 'sent') {
336
+ changed = true;
337
+ return { ...effect, status: 'success', messageId: state.messageId };
338
+ }
339
+ if (state.status === 'failed') {
340
+ changed = true;
341
+ deliveryFailure ??= { code: String(state.code ?? 'OUTBOX_DELIVERY_FAILED'), message: state.error };
342
+ return { ...effect, status: 'failed', error: state.error, ...(state.code !== undefined ? { code: state.code } : {}) };
343
+ }
344
+ if (state.status === 'missing' && isAunEffect(effect)) {
345
+ const messageId = findDeliveredEffectMessageId(audit.agentAid, effect);
346
+ changed = true;
347
+ if (messageId) {
348
+ return { ...effect, status: 'success', messageId };
349
+ }
350
+ const code = 'OUTBOX_RESULT_MISSING';
351
+ const error = 'outbox delivery result is missing and no remote message_id was recorded';
352
+ deliveryFailure ??= { code, message: error };
353
+ return { ...effect, status: 'failed', error, code };
354
+ }
355
+ return effect;
356
+ });
357
+ if (!changed)
358
+ return audit;
359
+ if (!deliveryFailure) {
360
+ const hasQueuedEffect = effects.some(effect => effect.status === 'queued');
361
+ const hasFailedEffect = effects.some(effect => effect.status === 'failed');
362
+ const canRestoreReplyStatus = audit.status === 'degraded'
363
+ && !hasQueuedEffect
364
+ && !hasFailedEffect
365
+ && !audit.anomalies?.length;
366
+ const status = canRestoreReplyStatus
367
+ ? audit.reply?.outcome === 'noop' ? 'noop' : 'completed'
368
+ : audit.status;
369
+ return { ...audit, status, effects };
370
+ }
371
+ return {
372
+ ...audit,
373
+ status: 'failed',
374
+ reason: deliveryFailure.code,
375
+ effects,
376
+ error: deliveryFailure,
377
+ };
378
+ }
379
+ function isAunEffect(effect) {
380
+ return effect.channelType === 'aun' || effect.channelKey?.startsWith('aun#') === true;
381
+ }
382
+ function findDeliveredEffectMessageId(agentAid, effect) {
383
+ if (!effect.operationId || !effect.channelId)
384
+ return undefined;
385
+ const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', effect.channelId, agentAid);
386
+ return findMessageLogOperationMessageId(chatDir, effect.operationId);
387
+ }
313
388
  function listAuditLogFiles(logDir) {
314
389
  let entries;
315
390
  try {
@@ -1,13 +1,13 @@
1
- const HISTORY_CHANNELS = new Set(['aun', 'feishu', 'wechat', 'dingtalk', 'qqbot', 'wecom']);
2
1
  /**
3
2
  * Turns legacy raw session-file references into the controlled history CLI.
4
3
  * The replacement deliberately keeps the explicit peer/channel scope but
5
4
  * never carries a filesystem location into a Trigger prompt.
6
5
  */
7
6
  export function rewriteLegacyTriggerSessionPaths(prompt, triggerAid) {
7
+ const historyChannels = new Set(['aun', 'feishu', 'wechat', 'dingtalk', 'qqbot', 'wecom']);
8
8
  const legacyPath = /data\/sessions\/([A-Za-z0-9_-]+)\/([^/\s`"'<>]+)(?:\/([^/\s`"'<>]+))?\/messages\.jsonl/g;
9
9
  return prompt.replace(legacyPath, (match, channel, first, second) => {
10
- if (!HISTORY_CHANNELS.has(channel))
10
+ if (!historyChannels.has(channel))
11
11
  return match;
12
12
  const isAun = channel === 'aun';
13
13
  const target = isAun ? second : first;
@@ -113,6 +113,7 @@ const PERMISSION_MODES = new Set([
113
113
  'auto',
114
114
  'request',
115
115
  'bypass',
116
+ 'fullaccess',
116
117
  ]);
117
118
  const TRIGGER_EFFORTS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
118
119
  function parsePermissionModeFlag(flags, opts = {}) {
@@ -124,10 +125,10 @@ function parsePermissionModeFlag(flags, opts = {}) {
124
125
  if (raw === 'inherit') {
125
126
  if (opts.allowInherit)
126
127
  return { ok: true, value: null };
127
- return { ok: false, error: '--permission 创建时只能接受 readonly、auto、request 或 bypass' };
128
+ return { ok: false, error: '--permission 创建时只能接受 readonly、auto、request、bypassfullaccess' };
128
129
  }
129
130
  if (!PERMISSION_MODES.has(raw)) {
130
- return { ok: false, error: '--permission 只接受 readonly、auto、request 或 bypass' };
131
+ return { ok: false, error: '--permission 只接受 readonly、auto、request、bypassfullaccess' };
131
132
  }
132
133
  return { ok: true, value: raw };
133
134
  }
@@ -13,6 +13,7 @@ const PERMISSION_MODES = new Set([
13
13
  'auto',
14
14
  'request',
15
15
  'bypass',
16
+ 'fullaccess',
16
17
  ]);
17
18
  export function normalizeTriggerDefinition(input, opts = {}) {
18
19
  if (!isObject(input))
@@ -234,6 +235,7 @@ function normalizeV3Phase2(raw, opts) {
234
235
  enabled: raw.enabled === undefined ? true : requiredBoolean(raw.enabled, 'enabled'),
235
236
  name,
236
237
  description: optionalString(raw.description),
238
+ authorizedBy: optionalString(raw.authorizedBy),
237
239
  createdAt,
238
240
  updatedAt,
239
241
  origin: normalizeOrigin(raw.origin),
@@ -438,6 +440,9 @@ function normalizeExecution(value, _opts) {
438
440
  onError: normalizeOnError(raw.onError),
439
441
  noopSentinel: optionalString(raw.noopSentinel) ?? DEFAULT_NOOP_SENTINEL,
440
442
  };
443
+ if (execution.type === 'script' && execution.permissionMode === 'fullaccess') {
444
+ throw new Error('script Trigger already executes as the daemon OS user and cannot declare execution.permissionMode=fullaccess');
445
+ }
441
446
  return execution;
442
447
  }
443
448
  function normalizeExecutionBaseagent(value) {
@@ -478,7 +483,7 @@ function normalizeTriggerPermissionMode(value) {
478
483
  if (mode === 'edit' || mode === 'noask' || mode === 'plan') {
479
484
  return normalizePermissionModeContract(mode).mode;
480
485
  }
481
- throw new Error('execution.permissionMode must be one of readonly, auto, request, bypass');
486
+ throw new Error('execution.permissionMode must be one of readonly, auto, request, bypass, fullaccess');
482
487
  }
483
488
  function normalizeScript(value) {
484
489
  if (!isObject(value))
@@ -75,6 +75,33 @@ export function atomicWrite(filePath, content) {
75
75
  export function atomicWriteJson(filePath, value) {
76
76
  atomicWrite(filePath, JSON.stringify(value, null, 2) + '\n');
77
77
  }
78
+ /**
79
+ * Update a small protected file without replacing its directory entry.
80
+ *
81
+ * H-class sandbox mounts bind individual files by pathname. Replacing such a
82
+ * file with rename(2) changes the inode while bubblewrap is constructing its
83
+ * mount table and can make the bind/remount operation fail. This helper keeps
84
+ * the inode stable; callers should use it only for best-effort, daemon-owned
85
+ * state where a crash during the write may leave incomplete content.
86
+ */
87
+ export function stableWrite(filePath, content) {
88
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
89
+ const noFollow = fs.constants.O_NOFOLLOW ?? 0;
90
+ const fd = fs.openSync(filePath, fs.constants.O_RDWR | fs.constants.O_CREAT | noFollow, 0o600);
91
+ try {
92
+ const stat = fs.fstatSync(fd);
93
+ if (!stat.isFile())
94
+ throw new Error(`stable write target is not a regular file: ${filePath}`);
95
+ fs.ftruncateSync(fd, 0);
96
+ fs.writeFileSync(fd, content, 'utf8');
97
+ }
98
+ finally {
99
+ fs.closeSync(fd);
100
+ }
101
+ }
102
+ export function stableWriteJson(filePath, value) {
103
+ stableWrite(filePath, JSON.stringify(value, null, 2) + '\n');
104
+ }
78
105
  /**
79
106
  * 原子读取——自动按状态恢复。文件不存在时返回 null。
80
107
  *
@@ -3,6 +3,7 @@ import path from 'path';
3
3
  import { compareVersions, resolveGlobalPkg } from './npm-ops.js';
4
4
  import { isWindows as platformIsWindows, resolveCommandPath } from './cross-platform.js';
5
5
  import { MIN_ECWEB_VERSION, WEB_CLI_BIN, WEB_PACKAGE_NAME } from '../product.js';
6
+ import { resolvePaths } from '../paths.js';
6
7
  /** EC Web owns the session-log reader, so older builds cannot safely serve a newer daemon. */
7
8
  export function isEcwebVersionSupported(version) {
8
9
  const normalized = version?.trim();
@@ -70,10 +71,23 @@ export function resolveEcwebLaunchCommand(ecwebArgs, opts = {}) {
70
71
  * 配对码是 ecweb 进程自己生成并持有的内部状态。daemon/CLI 通过 ecweb 的
71
72
  * localhost-only HTTP 接口 GET /api/pair-code 取当前码(远程访问被 ecweb 403 拒绝)。
72
73
  */
73
- /** 经 localhost 拉取 ecweb 当前配对码(仅本机可取)。失败返回 null。 */
74
- export async function fetchEcwebPairCode(port) {
74
+ /** 经 localhost 拉取 ecweb 当前配对码(仅本机可取)。失败返回 null。
75
+ * authorizedBy 仅由 daemon 的 AUN 控制通道传入,用于把配对码换出的 token
76
+ * 绑定到实际批准的 daemon-owner;普通 CLI 取码不携带该字段。 */
77
+ export async function fetchEcwebPairCode(port, authorizedBy) {
75
78
  try {
79
+ const headers = {};
80
+ const owner = authorizedBy?.trim();
81
+ if (owner)
82
+ headers['x-evolcore-pair-authorized-by'] = owner;
83
+ try {
84
+ const controlToken = fs.readFileSync(resolvePaths().controlToken, 'utf8').trim();
85
+ if (controlToken)
86
+ headers['x-evolcore-control-token'] = controlToken;
87
+ }
88
+ catch { }
76
89
  const resp = await fetch(`http://127.0.0.1:${port}/api/pair-code`, {
90
+ headers,
77
91
  signal: AbortSignal.timeout(2000),
78
92
  });
79
93
  if (!resp.ok)
@@ -388,7 +388,10 @@ export function getErrorMessage(error, terminalReason, includeEmoji = true) {
388
388
  const warnPrefix = includeEmoji ? '⚠️ ' : '';
389
389
  const errPrefix = includeEmoji ? '❌ ' : '';
390
390
  if (msg.includes('CONTEXT_COMPACT_FAILED') || isContextTooLongText(msg)) {
391
- return `${warnPrefix}上下文过长,自动压缩失败,请手动输入 /compact 重试`;
391
+ const detail = msg.match(/^CONTEXT_COMPACT_FAILED:\s*(.+)$/)?.[1]?.trim();
392
+ return detail
393
+ ? `${warnPrefix}上下文过长,自动压缩失败:${detail}`
394
+ : `${warnPrefix}上下文过长,自动压缩失败,请手动输入 /compact 重试`;
392
395
  }
393
396
  if (isSandboxInitializationFailure(error)) {
394
397
  return `${warnPrefix}只读执行沙箱启动失败(sandbox_initialization_failed),当前任务未执行;请检查 user namespace/seccomp 配置后重试`;
@@ -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 { normalizePermissionMode } from '../core/permission/mode.js';
5
6
  import { buildToolLifecycleEventKey } from '../core/audit/event-key.js';
6
7
  let currentLevel = process.env.LOG_LEVEL || 'INFO';
7
8
  const LEVELS = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3 };
@@ -70,8 +71,25 @@ export function normalizeStructuredLog(data) {
70
71
  ?? nested?.toolUseId
71
72
  ?? nested?.tool_use_id;
72
73
  const sessionId = data.sessionId ?? data.session_id ?? nested?.sessionId ?? nested?.session_id;
73
- const agentAid = data.agentAid ?? data.agent_aid ?? data.selfAid ?? nested?.agentAid ?? nested?.agent_aid;
74
- const permissionMode = data.permissionMode ?? data.permission_mode ?? nested?.permissionMode ?? nested?.permission_mode;
74
+ const agentAid = data.agentAid ?? data.agent_aid ?? data.selfAid ?? nested?.agentAid ?? nested?.agent_aid ?? nested?.selfAid;
75
+ const rawAgentName = data.agentName ?? data.agent_name ?? nested?.agentName ?? nested?.agent_name;
76
+ // Lifecycle streams should carry both the stable AID and a display label.
77
+ // When a producer only knows the AID, use that same value as the label so
78
+ // daily joins never invent a second identity for one session.
79
+ const normalizedAgentName = typeof rawAgentName === 'string' ? rawAgentName.trim() : '';
80
+ const agentName = normalizedAgentName
81
+ && !['unknown', '<unknown>', '<none>', 'undefined', 'null'].includes(normalizedAgentName)
82
+ ? normalizedAgentName
83
+ : typeof agentAid === 'string' && agentAid !== 'unknown'
84
+ ? agentAid
85
+ : undefined;
86
+ const rawPermissionMode = data.permissionMode ?? data.permission_mode ?? nested?.permissionMode ?? nested?.permission_mode;
87
+ // Normalize legacy values at the log boundary. Preserve an explicit
88
+ // `unknown` marker for records that genuinely arrived without context.
89
+ const permissionMode = typeof rawPermissionMode === 'string'
90
+ && rawPermissionMode !== 'unknown'
91
+ ? normalizePermissionMode(rawPermissionMode).mode
92
+ : rawPermissionMode;
75
93
  const toolName = data.toolName ?? data.tool ?? data.name ?? nested?.toolName ?? nested?.name;
76
94
  const isToolResult = eventType === 'tool:result' || eventType === 'tool_result';
77
95
  const isToolUse = eventType === 'tool:use' || eventType === 'tool_use';
@@ -116,6 +134,7 @@ export function normalizeStructuredLog(data) {
116
134
  ...(correlationId ? { correlationId } : {}),
117
135
  ...(sessionId ? { sessionId } : lifecycleRecord ? { sessionId: 'unknown' } : {}),
118
136
  ...(agentAid ? { agentAid } : lifecycleRecord ? { agentAid: 'unknown' } : {}),
137
+ ...(agentName ? { agentName } : lifecycleRecord ? { agentName: 'unknown' } : {}),
119
138
  ...(permissionMode ? { permissionMode } : lifecycleRecord ? { permissionMode: 'unknown' } : {}),
120
139
  ...(toolName ? { toolName } : {}),
121
140
  ...(decision ? { decision } : {}),
@@ -1,5 +1,6 @@
1
1
  import fs from 'fs';
2
2
  import { spawnSync } from 'child_process';
3
+ import { parseCimDate } from './process-introspect.js';
3
4
  function runnerKind(name) {
4
5
  const normalized = name.toLowerCase();
5
6
  if (normalized.includes('codex'))
@@ -164,7 +165,7 @@ function readUnixPsMetrics() {
164
165
  function readWindowsMetrics() {
165
166
  const command = [
166
167
  'Get-CimInstance Win32_Process',
167
- 'Select-Object ProcessId,ParentProcessId,Name,WorkingSetSize,KernelModeTime,UserModeTime,CreationDate',
168
+ "Select-Object ProcessId,ParentProcessId,Name,WorkingSetSize,KernelModeTime,UserModeTime,@{Name='CreationDate';Expression={if ($_.CreationDate) {$_.CreationDate.ToUniversalTime().ToString('o')}}}",
168
169
  'ConvertTo-Json -Compress',
169
170
  ].join(' | ');
170
171
  const result = spawnSync('powershell', ['-NoProfile', '-Command', command], {
@@ -184,13 +185,32 @@ function readWindowsMetrics() {
184
185
  name: String(row.Name || 'unknown'),
185
186
  rss: Number(row.WorkingSetSize) || 0,
186
187
  cumulativeCpuMs: ((Number(row.KernelModeTime) || 0) + (Number(row.UserModeTime) || 0)) / 10_000,
187
- startId: row.CreationDate ? String(row.CreationDate) : undefined,
188
+ // Keep a comparable numeric start time when PowerShell serializes CIM
189
+ // DateTime as either CIM text or an ISO date. The raw value remains as
190
+ // a fallback so PID reuse protection in ProcessTreeSampler still works
191
+ // when a platform returns an unfamiliar representation.
192
+ startId: parseWindowsCreationDate(row.CreationDate),
188
193
  })).filter((row) => row.pid > 0);
189
194
  }
190
195
  catch {
191
196
  return [];
192
197
  }
193
198
  }
199
+ export function parseWindowsCreationDate(value) {
200
+ if (value === undefined || value === null)
201
+ return undefined;
202
+ const raw = String(value).trim();
203
+ if (!raw)
204
+ return undefined;
205
+ const cim = parseCimDate(raw);
206
+ if (cim !== null)
207
+ return cim;
208
+ const serializedDate = raw.match(/^\/Date\((-?\d+)(?:[+-]\d{4})?\)\/$/);
209
+ if (serializedDate)
210
+ return Number(serializedDate[1]);
211
+ const parsed = Date.parse(raw);
212
+ return Number.isFinite(parsed) ? parsed : raw;
213
+ }
194
214
  export function readProcessMetrics() {
195
215
  if (process.platform === 'linux')
196
216
  return readLinuxMetrics();
@@ -209,9 +229,9 @@ export class ProcessTreeSampler {
209
229
  this.readMetrics = readMetrics;
210
230
  this.now = now;
211
231
  }
212
- sample(associatedRootPids = []) {
232
+ sample(associatedRootPids = [], metricsOverride) {
213
233
  const timestamp = this.now();
214
- const metrics = this.readMetrics();
234
+ const metrics = metricsOverride ?? this.readMetrics();
215
235
  const byPid = new Map(metrics.map(metric => [metric.pid, metric]));
216
236
  const children = new Map();
217
237
  for (const metric of metrics) {
@@ -0,0 +1,31 @@
1
+ import { parentPort } from 'node:worker_threads';
2
+ import { performance } from 'node:perf_hooks';
3
+ import { readProcessMetrics } from './process-tree-stats.js';
4
+ const port = parentPort;
5
+ if (!port)
6
+ throw new Error('process-tree worker requires a parent port');
7
+ port.on('message', (message) => {
8
+ if (message?.type !== 'sample')
9
+ return;
10
+ const startedAt = performance.now();
11
+ try {
12
+ const metrics = readProcessMetrics();
13
+ if (metrics.length === 0)
14
+ throw new Error('process enumeration returned no metrics');
15
+ const response = {
16
+ type: 'sample',
17
+ requestId: message.requestId,
18
+ metrics,
19
+ durationMs: Math.round((performance.now() - startedAt) * 10) / 10,
20
+ };
21
+ port.postMessage(response);
22
+ }
23
+ catch (error) {
24
+ port.postMessage({
25
+ type: 'sample-error',
26
+ requestId: message.requestId,
27
+ error: error instanceof Error ? error.message : String(error),
28
+ durationMs: Math.round((performance.now() - startedAt) * 10) / 10,
29
+ });
30
+ }
31
+ });