evolcore 0.0.11 → 0.0.13

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 (164) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/bin/codex-managed-hook.mjs +80 -0
  3. package/bin/ec-safe-output.js +26 -7
  4. package/dist/agents/claude-runner.js +86 -7
  5. package/dist/agents/codex-app-server-client.js +174 -20
  6. package/dist/agents/codex-runner.js +375 -92
  7. package/dist/agents/ecagent-runner.js +39 -4
  8. package/dist/agents/gemini-runner.js +1 -0
  9. package/dist/aun/aid/client.js +11 -21
  10. package/dist/aun/aid/control-aid.js +36 -13
  11. package/dist/aun/aid/managed-operation.js +107 -0
  12. package/dist/aun/aid/store.js +13 -7
  13. package/dist/aun/group-fs-download.js +56 -0
  14. package/dist/aun/group-identity.js +1 -2
  15. package/dist/aun/msg/group.js +91 -44
  16. package/dist/aun/msg/index.js +1 -2
  17. package/dist/aun/msg/managed-operation.js +865 -0
  18. package/dist/aun/msg/mention-schema.js +20 -0
  19. package/dist/aun/msg/p2p.js +7 -0
  20. package/dist/aun/msg/upload.js +16 -1
  21. package/dist/aun/outbox.js +31 -2
  22. package/dist/aun/rpc/client.js +66 -0
  23. package/dist/aun/rpc/index.js +1 -2
  24. package/dist/aun/storage/{upload.js → client.js} +11 -2
  25. package/dist/aun/storage/index.js +1 -2
  26. package/dist/channels/aun.js +655 -240
  27. package/dist/channels/daemon.js +13 -0
  28. package/dist/cli/agent-command.js +56 -8
  29. package/dist/cli/agent.js +108 -31
  30. package/dist/cli/aun-commands.js +270 -69
  31. package/dist/cli/bench.js +12 -3
  32. package/dist/cli/cli-argv.js +10 -0
  33. package/dist/cli/config.js +9 -17
  34. package/dist/cli/ctl-command.js +1 -1
  35. package/dist/cli/daemon-commands.js +70 -55
  36. package/dist/cli/fs-command.js +79 -10
  37. package/dist/cli/handoff-command.js +3 -0
  38. package/dist/cli/index.js +30 -2
  39. package/dist/cli/init.js +77 -8
  40. package/dist/cli/managed-command-guard.js +41 -0
  41. package/dist/cli/model.js +11 -4
  42. package/dist/cli/queue-command.js +53 -1
  43. package/dist/cli/response.js +71 -0
  44. package/dist/cli/restart-monitor.js +7 -32
  45. package/dist/cli/trigger-command.js +11 -0
  46. package/dist/config/access-policy.js +1 -2
  47. package/dist/config/builtin-roles.js +9 -0
  48. package/dist/config/config-manager.js +21 -1
  49. package/dist/config/contact-book-store.js +11 -3
  50. package/dist/config/contact-request-service.js +161 -15
  51. package/dist/config/mention-mode.js +8 -24
  52. package/dist/config/peer-role-resolver.js +6 -2
  53. package/dist/config/role-migration-startup.js +53 -0
  54. package/dist/config/role-store.js +1 -1
  55. package/dist/core/auth/agent-delegation.js +1 -2
  56. package/dist/core/auth/auth-gateway.js +29 -1
  57. package/dist/core/auth/authorization-audit.js +15 -0
  58. package/dist/core/auth/operation-authorizer.js +31 -7
  59. package/dist/core/auth/operation-catalog.js +196 -25
  60. package/dist/core/bootstrap-messages.js +25 -0
  61. package/dist/core/bootstrap-service.js +64 -4
  62. package/dist/core/capability/providers/claude-capability-provider.js +9 -31
  63. package/dist/core/capability/providers/codex-capability-provider.js +4 -15
  64. package/dist/core/capability/skill-discovery.js +55 -0
  65. package/dist/core/channel-loader.js +1 -1
  66. package/dist/core/command/cli-intent-parser.js +9 -0
  67. package/dist/core/command/command-handler.js +650 -42
  68. package/dist/core/command/connect-menu.js +15 -0
  69. package/dist/core/command/group-menu.js +4 -4
  70. package/dist/core/command/menu-catalog.js +370 -0
  71. package/dist/core/command/menu-handler.js +230 -51
  72. package/dist/core/command/menu-protocol.js +94 -2
  73. package/dist/core/command/slash-gate.js +5 -2
  74. package/dist/core/command/slash-handler.js +99 -31
  75. package/dist/core/daemon-file-cache.js +2 -1
  76. package/dist/core/data-migration.js +9 -2
  77. package/dist/core/event-catalog.js +6 -0
  78. package/dist/core/evolagent.js +4 -2
  79. package/dist/core/handoff/dispatcher.js +5 -2
  80. package/dist/core/handoff/runtime.js +157 -22
  81. package/dist/core/handoff/store.js +29 -1
  82. package/dist/core/handoff/types.js +1 -0
  83. package/dist/core/interaction-router.js +10 -0
  84. package/dist/core/message/im-renderer.js +19 -0
  85. package/dist/core/message/logical-queue-bridge.js +1 -0
  86. package/dist/core/message/mention-schema.js +126 -0
  87. package/dist/core/message/message-bridge.js +298 -88
  88. package/dist/core/message/message-log.js +3 -0
  89. package/dist/core/message/message-queue.js +268 -39
  90. package/dist/core/message/message-utils.js +81 -4
  91. package/dist/core/message/response-engine.js +161 -77
  92. package/dist/core/message/stream-debouncer.js +10 -2
  93. package/dist/core/permission/approval-gateway.js +1 -0
  94. package/dist/core/permission/ec-command-parser.js +243 -18
  95. package/dist/core/permission/index.js +1 -1
  96. package/dist/core/permission/mode.js +15 -0
  97. package/dist/core/permission/readonly-shell-query.js +148 -8
  98. package/dist/core/permission/sandbox-runtime.js +43 -1
  99. package/dist/core/permission/tool-policy.js +322 -57
  100. package/dist/core/protected-paths.js +114 -4
  101. package/dist/core/session/session-manager.js +42 -4
  102. package/dist/core/session/session-mapper.js +20 -1
  103. package/dist/eck/detect.js +18 -1
  104. package/dist/eck/group-rules-sync.js +15 -44
  105. package/dist/eck/manifest-engine.js +39 -13
  106. package/dist/index.js +541 -180
  107. package/dist/ipc.js +232 -10
  108. package/dist/paths.js +3 -0
  109. package/dist/response-system/coordinator.js +6 -4
  110. package/dist/response-system/engines/v1/proactive-flow.js +26 -8
  111. package/dist/response-system/modes/single-session/index.js +14 -1
  112. package/dist/response-system/selector.js +6 -5
  113. package/dist/trigger/anomaly-store.js +4 -0
  114. package/dist/trigger/feedback.js +64 -11
  115. package/dist/trigger/manager.js +21 -1
  116. package/dist/trigger/parser.js +14 -2
  117. package/dist/trigger/scheduler.js +1 -0
  118. package/dist/trigger/script-executor.js +1 -0
  119. package/dist/trigger/validation.js +67 -6
  120. package/dist/utils/codex-app-server-registry.js +90 -0
  121. package/dist/utils/codex-cli.js +5 -1
  122. package/dist/utils/cross-platform.js +15 -0
  123. package/dist/utils/ecweb-supervisor.js +332 -0
  124. package/dist/utils/error-utils.js +10 -0
  125. package/dist/utils/npm-ops.js +5 -12
  126. package/dist/utils/tool-summary.js +11 -0
  127. package/kits/docs/channels/aun.md +1 -1
  128. package/kits/docs/context-assembly.md +2 -2
  129. package/kits/docs/evolcore/agent.md +11 -6
  130. package/kits/docs/evolcore/aid.md +14 -0
  131. package/kits/docs/evolcore/trigger.md +17 -1
  132. package/kits/docs/prompt-loading-architecture.md +1 -2
  133. package/kits/docs/venues/group.md +1 -1
  134. package/kits/eck_manifest.json +0 -12
  135. package/kits/migrations/migrate-contact-book-v2.mjs +1 -2
  136. package/kits/rules/04-relation.md +1 -1
  137. package/kits/rules/05-venue.md +2 -2
  138. package/kits/schemas/role-config.schema.1.json +1 -0
  139. package/kits/schemas/single-session.schema.1.json +3 -3
  140. package/kits/schemas/single-session.schema.2.json +3 -3
  141. package/kits/templates/roles/admin.json +27 -0
  142. package/kits/templates/roles/member.json +34 -1
  143. package/kits/templates/roles/visitor.json +35 -3
  144. package/kits/templates/system-fragments/bootstrap.md +1 -1
  145. package/kits/templates/system-fragments/channel.md +1 -1
  146. package/kits/templates/system-fragments/session.md +1 -1
  147. package/package.json +2 -2
  148. package/dist/aun/msg/payload-type.js +0 -27
  149. package/dist/aun/rpc/caller.js +0 -42
  150. package/dist/aun/rpc/connection.js +0 -25
  151. package/dist/aun/storage/manage.js +0 -10
  152. package/dist/config/access-policy-domain.js +0 -18
  153. package/dist/config/config-batch-get.js +0 -11
  154. package/dist/config/owner-policy.js +0 -4
  155. package/dist/config/role-config-v4-startup.js +0 -32
  156. package/dist/config/role-config-v5-startup.js +0 -27
  157. package/dist/core/auth/trigger-authorization.js +0 -15
  158. package/dist/core/command/evol-menu-version-gate.js +0 -39
  159. package/dist/core/interaction-registration.js +0 -10
  160. package/dist/core/permission/execution-sandbox.js +0 -16
  161. package/dist/core/relation/peer-key.js +0 -1
  162. package/dist/core/session/session-key.js +0 -24
  163. package/dist/eck/baseagent-caps.js +0 -18
  164. package/dist/eck/rules-loader.js +0 -28
@@ -1,6 +1,5 @@
1
1
  import crypto from 'crypto';
2
2
  import fs from 'fs';
3
- import path from 'path';
4
3
  import { isValidAid } from '../aun/aid/validation.js';
5
4
  import { agentContactOperations } from '../paths.js';
6
5
  import { listStaticAgentAdmins, listStaticAgentOwners } from './peer-role-resolver.js';
@@ -12,6 +11,7 @@ export const CONTACT_REQUEST_MAX_PER_WINDOW = 3;
12
11
  export const CONTACT_REQUEST_MAX_PENDING = 100;
13
12
  export const CONTACT_REQUEST_MAX_RETAINED = 1000;
14
13
  export const CONTACT_REQUEST_NOTE_MAX_LENGTH = 500;
14
+ export const CONTACT_REQUEST_ID_MAX_LENGTH = 128;
15
15
  export async function mutateContactWithOperation(input) {
16
16
  const at = new Date(input.now ?? Date.now()).toISOString();
17
17
  const logFile = agentContactOperations(input.selfAid);
@@ -26,7 +26,7 @@ export async function mutateContactWithOperation(input) {
26
26
  mutation: input.mutation,
27
27
  actor: input.actorId,
28
28
  value: undefined,
29
- beforeCommit: ({ contactRevision: nextRevision }) => appendOperationRecords(logFile, [operationRecord({
29
+ beforeCommit: ({ contactRevision: nextRevision }) => operationLogChange(logFile, [operationRecord({
30
30
  at,
31
31
  op: input.operation,
32
32
  primaryId: input.primaryId,
@@ -73,7 +73,17 @@ export async function submitContactRequest(input) {
73
73
  && Date.parse(record.at) > windowStart
74
74
  && Date.parse(record.at) <= nowMs)).length;
75
75
  if (recentCount >= CONTACT_REQUEST_MAX_PER_WINDOW) {
76
- return preparedWithExpirations(expiration, { code: 'rate-limited', contactRevision: '' }, logFile);
76
+ return preparedWithExpirations(expiration, {
77
+ code: 'rate-limited',
78
+ ...(effectiveEntry?.status === 'pending' && effectiveEntry.pendingRequest
79
+ ? {
80
+ requestId: effectiveEntry.pendingRequest.id,
81
+ submittedAt: effectiveEntry.pendingRequest.submittedAt,
82
+ expiresAt: effectiveEntry.pendingRequest.expiresAt,
83
+ }
84
+ : {}),
85
+ contactRevision: '',
86
+ }, logFile);
77
87
  }
78
88
  const counts = countRequestRecordsAfterExpirations(contact, nowMs);
79
89
  const isExistingPending = effectiveEntry?.status === 'pending';
@@ -103,13 +113,14 @@ export async function submitContactRequest(input) {
103
113
  mutation: { type: 'batch', mutations },
104
114
  actor: applicantAid,
105
115
  value,
106
- beforeCommit: ({ contactRevision }) => appendOperationRecords(logFile, [
116
+ beforeCommit: ({ contactRevision }) => operationLogChange(logFile, [
107
117
  ...expiration.events.map(event => ({ ...event, contactRevision })),
108
118
  operationRecord({
109
119
  at: nowIso,
110
120
  op,
111
121
  primaryId: applicantAid,
112
122
  requestId,
123
+ expiresAt,
113
124
  sourceChannelKey: input.sourceChannelKey,
114
125
  ...(note ? { note } : {}),
115
126
  contactRevision,
@@ -143,6 +154,10 @@ export async function reviewContactRequest(input) {
143
154
  if (!entry || entry.status !== 'pending' || entry.pendingRequest?.id !== input.requestId) {
144
155
  return preparedNoop({ code: 'request-stale', contactRevision });
145
156
  }
157
+ const requestMetadata = {
158
+ requestId: entry.pendingRequest.id,
159
+ expiresAt: entry.pendingRequest.expiresAt,
160
+ };
146
161
  if (Date.parse(entry.pendingRequest.expiresAt) <= nowMs) {
147
162
  const mutation = {
148
163
  type: 'expire-request',
@@ -153,8 +168,8 @@ export async function reviewContactRequest(input) {
153
168
  return {
154
169
  mutation,
155
170
  actor: input.actorId,
156
- value: { code: 'request-expired', contactRevision },
157
- beforeCommit: ({ contactRevision: nextRevision }) => appendOperationRecords(logFile, [operationRecord({
171
+ value: { code: 'request-expired', contactRevision, ...requestMetadata },
172
+ beforeCommit: ({ contactRevision: nextRevision }) => operationLogChange(logFile, [operationRecord({
158
173
  at: nowIso,
159
174
  op: 'expire',
160
175
  primaryId: input.primaryId,
@@ -177,8 +192,8 @@ export async function reviewContactRequest(input) {
177
192
  now: nowIso,
178
193
  },
179
194
  actor: input.actorId,
180
- value: { code, contactRevision },
181
- beforeCommit: ({ contactRevision: nextRevision }) => appendOperationRecords(logFile, [operationRecord({
195
+ value: { code, contactRevision, ...requestMetadata },
196
+ beforeCommit: ({ contactRevision: nextRevision }) => operationLogChange(logFile, [operationRecord({
182
197
  at: nowIso,
183
198
  op: input.decision === 'approve' ? 'approve' : input.decision === 'reject' ? 'reject' : 'block',
184
199
  primaryId: input.primaryId,
@@ -208,7 +223,7 @@ export async function expirePendingContactRequests(selfAid, now = Date.now()) {
208
223
  mutation: { type: 'batch', mutations: expiration.mutations },
209
224
  actor: 'contact-request-expiry',
210
225
  value: expiration.mutations.length,
211
- beforeCommit: ({ contactRevision }) => appendOperationRecords(logFile, expiration.events.map(event => ({ ...event, contactRevision }))),
226
+ beforeCommit: ({ contactRevision }) => operationLogChange(logFile, expiration.events.map(event => ({ ...event, contactRevision }))),
212
227
  };
213
228
  });
214
229
  return result.value;
@@ -226,6 +241,91 @@ export async function listPendingContactRequests(selfAid, now = Date.now()) {
226
241
  }))));
227
242
  return result.value;
228
243
  }
244
+ /** Return only the authenticated applicant's own admission/request state. */
245
+ export async function queryOwnContactRequestStatus(input) {
246
+ const applicantAid = String(input.applicantAid || '').trim();
247
+ if (!isValidAid(applicantAid))
248
+ return protocolResult('NOT_ALLOWED');
249
+ const now = input.now ?? Date.now();
250
+ const rawRequestId = input.requestId;
251
+ if (rawRequestId !== undefined && typeof rawRequestId !== 'string')
252
+ return protocolResult('NOT_ALLOWED');
253
+ const requestedRequestId = rawRequestId?.trim();
254
+ if (requestedRequestId !== undefined
255
+ && (!requestedRequestId || requestedRequestId.length > CONTACT_REQUEST_ID_MAX_LENGTH)) {
256
+ return protocolResult('NOT_ALLOWED');
257
+ }
258
+ const owners = listAgentOwners(input.selfAid);
259
+ const result = await mutateContactBookPrepared(input.selfAid, ({ contact }) => {
260
+ const logFile = agentContactOperations(input.selfAid);
261
+ const expiration = collectExpiredMutations(contact, now);
262
+ const entry = effectiveEntryAfterExpirations(contact.contacts[applicantAid], now);
263
+ const operations = readOperationLog(logFile).filter(record => record.primaryId === applicantAid);
264
+ const latestRequestOperation = [...operations].reverse().find(record => (!!record.requestId
265
+ && (record.op === 'request' || record.op === 'resubmit' || record.op === 'approve'
266
+ || record.op === 'reject' || record.op === 'expire' || record.op === 'block')));
267
+ if (requestedRequestId !== undefined && latestRequestOperation?.requestId !== requestedRequestId) {
268
+ return preparedNoop(protocolResult('NOT_ALLOWED'));
269
+ }
270
+ const requestMetadata = requestMetadataFor(operations, latestRequestOperation?.requestId);
271
+ let value;
272
+ if (owners.includes(applicantAid)
273
+ || (entry && (entry.status === undefined || entry.status === 'active'))) {
274
+ value = protocolResult('APPROVED', requestMetadata);
275
+ }
276
+ else if (entry?.status === 'blocked') {
277
+ value = protocolResult('NOT_ALLOWED');
278
+ }
279
+ else if (entry?.status === 'pending' && entry.pendingRequest) {
280
+ value = protocolResult('PENDING', {
281
+ requestId: entry.pendingRequest.id,
282
+ expiresAt: entry.pendingRequest.expiresAt,
283
+ });
284
+ }
285
+ else if (entry?.status === 'declined') {
286
+ value = protocolResult('EXPIRED', {
287
+ requestId: latestRequestOperation?.op === 'expire' ? latestRequestOperation.requestId : requestMetadata.requestId,
288
+ expiresAt: entry.declinedAt ?? requestMetadata.expiresAt,
289
+ });
290
+ }
291
+ else if (latestRequestOperation?.op === 'reject') {
292
+ value = protocolResult('REJECTED', requestMetadata);
293
+ }
294
+ else if (latestRequestOperation?.op === 'expire') {
295
+ value = protocolResult('EXPIRED', requestMetadata);
296
+ }
297
+ else if (owners.length === 0) {
298
+ value = protocolResult('NOT_ALLOWED');
299
+ }
300
+ else {
301
+ value = protocolResult('CONTACT_REQUIRED');
302
+ }
303
+ if (expiration.mutations.length === 0)
304
+ return preparedNoop(value);
305
+ return {
306
+ mutation: { type: 'batch', mutations: expiration.mutations },
307
+ actor: 'contact-request-expiry',
308
+ value,
309
+ beforeCommit: ({ contactRevision }) => operationLogChange(logFile, expiration.events.map(event => ({ ...event, contactRevision }))),
310
+ };
311
+ });
312
+ return result.value;
313
+ }
314
+ export function contactRequestSubmissionProtocolResult(result) {
315
+ const metadata = { requestId: result.requestId, expiresAt: result.expiresAt };
316
+ switch (result.code) {
317
+ case 'submitted':
318
+ case 'resubmitted':
319
+ return protocolResult('SUBMITTED', metadata);
320
+ case 'already-contact':
321
+ case 'owner':
322
+ return protocolResult('APPROVED', metadata);
323
+ case 'rate-limited':
324
+ return protocolResult('RATE_LIMITED', metadata);
325
+ default:
326
+ return protocolResult('NOT_ALLOWED', metadata);
327
+ }
328
+ }
229
329
  export function findContactRequestNote(selfAid, requestId) {
230
330
  const records = readOperationLog(agentContactOperations(selfAid));
231
331
  return [...records].reverse().find(record => (record.requestId === requestId
@@ -237,6 +337,24 @@ function normalizeNote(value) {
237
337
  return null;
238
338
  return note;
239
339
  }
340
+ function requestMetadataFor(operations, requestId) {
341
+ if (!requestId)
342
+ return {};
343
+ const submitted = [...operations].reverse().find(record => (record.requestId === requestId && (record.op === 'request' || record.op === 'resubmit')));
344
+ const submittedAt = submitted ? Date.parse(submitted.at) : Number.NaN;
345
+ return {
346
+ requestId,
347
+ expiresAt: submitted?.expiresAt
348
+ ?? (Number.isFinite(submittedAt) ? new Date(submittedAt + CONTACT_REQUEST_TTL_MS).toISOString() : undefined),
349
+ };
350
+ }
351
+ function protocolResult(status, metadata = {}) {
352
+ return {
353
+ status,
354
+ requestId: metadata.requestId ?? null,
355
+ expiresAt: metadata.expiresAt ?? null,
356
+ };
357
+ }
240
358
  function collectExpiredMutations(book, now) {
241
359
  const mutations = [];
242
360
  const events = [];
@@ -286,7 +404,7 @@ function preparedWithExpirations(expiration, value, logFile) {
286
404
  mutation: { type: 'batch', mutations: expiration.mutations },
287
405
  actor: 'contact-request-expiry',
288
406
  value,
289
- beforeCommit: ({ contactRevision }) => appendOperationRecords(logFile, expiration.events.map(event => ({ ...event, contactRevision }))),
407
+ beforeCommit: ({ contactRevision }) => operationLogChange(logFile, expiration.events.map(event => ({ ...event, contactRevision }))),
290
408
  };
291
409
  }
292
410
  function preparedNoop(value) {
@@ -310,7 +428,14 @@ function readOperationLog(file) {
310
428
  const record = JSON.parse(line);
311
429
  if (!record || typeof record !== 'object' || typeof record.at !== 'string'
312
430
  || !Number.isFinite(Date.parse(record.at)) || typeof record.op !== 'string'
313
- || typeof record.primaryId !== 'string')
431
+ || !['request', 'resubmit', 'approve', 'reject', 'expire', 'block', 'unblock', 'manual-add'].includes(record.op)
432
+ || typeof record.primaryId !== 'string' || !isValidAid(record.primaryId)
433
+ || (record.requestId !== undefined && typeof record.requestId !== 'string')
434
+ || (record.expiresAt !== undefined && (!isValidIsoDate(record.expiresAt)))
435
+ || (record.actorId !== undefined && typeof record.actorId !== 'string')
436
+ || (record.sourceChannelKey !== undefined && typeof record.sourceChannelKey !== 'string')
437
+ || (record.note !== undefined && (typeof record.note !== 'string' || record.note.length > CONTACT_REQUEST_NOTE_MAX_LENGTH))
438
+ || typeof record.contactRevision !== 'string')
314
439
  throw new Error('invalid record');
315
440
  records.push(record);
316
441
  }
@@ -320,11 +445,32 @@ function readOperationLog(file) {
320
445
  }
321
446
  return records;
322
447
  }
323
- function appendOperationRecords(file, records) {
448
+ function isValidIsoDate(value) {
449
+ return Number.isFinite(Date.parse(value));
450
+ }
451
+ function operationLogChange(file, records) {
324
452
  if (records.length === 0)
325
- return;
326
- fs.mkdirSync(path.dirname(file), { recursive: true });
327
- fs.appendFileSync(file, `${records.map(record => JSON.stringify(record)).join('\n')}\n`, 'utf8');
453
+ return undefined;
454
+ // Validate the existing log while the Contact Book lock is held. The log is
455
+ // part of the same journal transaction as contact.json, so a malformed log
456
+ // must fail closed before either file is changed.
457
+ readOperationLog(file);
458
+ let before;
459
+ try {
460
+ before = fs.readFileSync(file, 'utf8');
461
+ }
462
+ catch (error) {
463
+ if (error?.code === 'ENOENT')
464
+ before = null;
465
+ else
466
+ throw error;
467
+ }
468
+ const prefix = before && !before.endsWith('\n') ? `${before}\n` : (before ?? '');
469
+ return {
470
+ file,
471
+ before,
472
+ after: `${prefix}${records.map(record => JSON.stringify(record)).join('\n')}\n`,
473
+ };
328
474
  }
329
475
  function operationRecord(input) {
330
476
  return { id: `evt_${crypto.randomUUID().replace(/-/g, '')}`, ...input };
@@ -1,27 +1,11 @@
1
- /**
2
- * mentionMode ↔ AUN 协议 dispatch_mode 词汇翻译(单一事实源)。
3
- *
4
- * 配置层用 mentionMode(disabled / mention-only),AUN 协议层用 dispatch_mode
5
- * (broadcast / mention)。二者语义等价,仅词汇不同:
6
- * - mention-only(只处理被 @ 的消息)≡ mention(仅被 @ 时响应)
7
- * - disabled(关闭 mention 过滤) ≡ broadcast(全部响应)
8
- *
9
- * 仅在协议边界(AUN resolver、render fallback)翻译;配置层、命令层一律用
10
- * mentionMode 词汇。
11
- */
12
- /** config → AUN 协议词汇;未知/未设值返回 undefined(回退服务端 dispatch_mode)。 */
13
- export function mentionModeToDispatch(mode) {
14
- if (mode === 'mention-only')
15
- return 'mention';
16
- if (mode === 'disabled')
17
- return 'broadcast';
18
- return undefined;
19
- }
20
- /** AUN 协议词汇 → config;未知返回 undefined。 */
21
- export function dispatchToMentionMode(dispatch) {
22
- if (dispatch === 'mention')
23
- return 'mention-only';
24
- if (dispatch === 'broadcast')
1
+ /** AUN `mention_mode` and EvolCore `mentionMode` share these wire values. */
2
+ export function normalizeMentionMode(mode) {
3
+ if (typeof mode !== 'string')
4
+ return undefined;
5
+ const normalized = mode.trim().toLowerCase();
6
+ if (normalized === 'disabled')
25
7
  return 'disabled';
8
+ if (normalized === 'mention-only' || normalized === 'mention_only')
9
+ return 'mention-only';
26
10
  return undefined;
27
11
  }
@@ -123,8 +123,12 @@ export function resolvePeerRoleDetail(ctx) {
123
123
  const winner = selectHighestRank(candidates, ctx.selfAid);
124
124
  if (winner)
125
125
  return resultFor(winner.role, winner.source, actor, ctx.selfAid);
126
- return roleRegistryExists(ctx.selfAid) && roleExists('visitor', ctx.selfAid)
127
- ? resultFor('visitor', 'builtin-fallback', actor, ctx.selfAid)
126
+ // Group members get the normal interactive member policy when no explicit
127
+ // group/actor/default assignment exists. Private conversations retain the
128
+ // read-only visitor fallback.
129
+ const fallbackRole = ctx.chatType === 'group' ? 'member' : 'visitor';
130
+ return roleRegistryExists(ctx.selfAid) && roleExists(fallbackRole, ctx.selfAid)
131
+ ? resultFor(fallbackRole, 'builtin-fallback', actor, ctx.selfAid)
128
132
  : resultFor(null, 'none', actor, ctx.selfAid);
129
133
  }
130
134
  export function roleToSessionIdentity(role) {
@@ -0,0 +1,53 @@
1
+ import path from 'path';
2
+ import { pathToFileURL } from 'url';
3
+ import { getPackageRoot, resolvePaths } from '../paths.js';
4
+ import { assertRoleConfigV4Ready, assertRoleConfigV5Ready, ConfigError } from './config-manager.js';
5
+ /** Apply the one-off v4 role config migration before daemon startup. */
6
+ export async function ensureRoleConfigV4OnStartup() {
7
+ try {
8
+ assertRoleConfigV4Ready();
9
+ return null;
10
+ }
11
+ catch (error) {
12
+ if (!(error instanceof ConfigError) || error.code !== 'ROLE_CONFIG_MIGRATION_REQUIRED')
13
+ throw error;
14
+ }
15
+ const home = resolvePaths().root;
16
+ const script = path.join(getPackageRoot(), 'kits', 'migrations', 'migrate-role-config-v4.mjs');
17
+ try {
18
+ const migration = await import(pathToFileURL(script).href);
19
+ const result = migration.runMigration({ home, apply: true });
20
+ assertRoleConfigV4Ready();
21
+ return result;
22
+ }
23
+ catch (error) {
24
+ const detail = error instanceof Error ? error.message : String(error);
25
+ throw new ConfigError('ROLE_CONFIG_MIGRATION_FAILED', `Automatic role config v4 migration failed: ${detail}\n` +
26
+ `Fix the reported config or run manually:\n` +
27
+ `node kits/migrations/migrate-role-config-v4.mjs --apply --home ${JSON.stringify(home)}`, { cause: error });
28
+ }
29
+ }
30
+ /** Apply the standalone v5 role registry migration before daemon startup. */
31
+ export async function ensureRoleConfigV5OnStartup() {
32
+ try {
33
+ assertRoleConfigV5Ready();
34
+ return null;
35
+ }
36
+ catch (error) {
37
+ if (!(error instanceof ConfigError) || error.code !== 'ROLE_CONFIG_V5_MIGRATION_REQUIRED')
38
+ throw error;
39
+ }
40
+ const home = resolvePaths().root;
41
+ const script = path.join(getPackageRoot(), 'kits', 'migrations', 'migrate-role-config-v5.mjs');
42
+ try {
43
+ const migration = await import(pathToFileURL(script).href);
44
+ const result = migration.runMigration({ home, apply: true });
45
+ assertRoleConfigV5Ready();
46
+ return result;
47
+ }
48
+ catch (error) {
49
+ const detail = error instanceof Error ? error.message : String(error);
50
+ throw new ConfigError('ROLE_CONFIG_V5_MIGRATION_FAILED', `Automatic standalone role migration failed: ${detail}\n` +
51
+ `node kits/migrations/migrate-role-config-v5.mjs --apply --home ${JSON.stringify(home)}`, { cause: error });
52
+ }
53
+ }
@@ -113,7 +113,7 @@ function readRegistry(selfAid, diagnostics) {
113
113
  code: 'INVALID_ROLE_ASSIGNMENT',
114
114
  roleId: selected,
115
115
  file: path.relative(resolvePaths().root, file),
116
- message: `defaultRoles.${scene} references invalid or unregistered role ${selected}; using visitor fallback`,
116
+ message: `defaultRoles.${scene} references invalid or unregistered role ${selected}; using ${scene === 'group' ? 'member' : 'visitor'} fallback`,
117
117
  });
118
118
  defaultRoles[scene] = null;
119
119
  }
@@ -2,7 +2,7 @@ import crypto from 'crypto';
2
2
  import { authorizeOperation, buildAuthSubject } from './auth-gateway.js';
3
3
  import { formatPeerKey, parsePeerKey } from '../relation/peer-identity.js';
4
4
  export const AGENT_DELEGATION_TOKEN_ENV = 'EVOLCORE_DELEGATION_TOKEN';
5
- export const AGENT_DELEGATION_COMMAND_TTL_MS = 60_000;
5
+ export const AGENT_DELEGATION_COMMAND_TTL_MS = 5 * 60_000;
6
6
  export class AgentDelegationRegistry {
7
7
  grantsByHash = new Map();
8
8
  activeHashBySession = new Map();
@@ -155,7 +155,6 @@ export function authorizeDelegatedAunMsgSend(registry, input) {
155
155
  channelId,
156
156
  chatType: grant.chatType,
157
157
  conversationId: channelId,
158
- identity: { role: grant.issuedRole, mode: 'interactive' },
159
158
  processOwners: [],
160
159
  fromControlChannel: false,
161
160
  });
@@ -4,6 +4,22 @@ import { formatPeerKey } from '../relation/peer-identity.js';
4
4
  import { authorizeCommand, authorizeResolvedConfigCommand } from './operation-authorizer.js';
5
5
  import { auditCommandAuthorization } from './authorization-audit.js';
6
6
  import { hasTrustedPrincipal } from './authenticated-actor.js';
7
+ import { resolveRuntimePermissionMode } from '../role/runtime-policy.js';
8
+ const CROSS_AGENT_READ_OPERATIONS = new Set([
9
+ 'trigger.list',
10
+ 'trigger.show',
11
+ 'trigger.history',
12
+ ]);
13
+ export function isCrossAgentTriggerOperationAllowed(input) {
14
+ if (input.control || input.taskAgentAid === input.targetAgentAid)
15
+ return true;
16
+ if (!input.taskAgentAid || !input.targetManagement)
17
+ return false;
18
+ return CROSS_AGENT_READ_OPERATIONS.has(input.operation);
19
+ }
20
+ export function isCrossAgentTriggerReadOperation(operation) {
21
+ return CROSS_AGENT_READ_OPERATIONS.has(operation);
22
+ }
7
23
  export function buildAuthSubject(input) {
8
24
  const chatType = input.chatType === 'group' ? 'group' : 'private';
9
25
  const actorId = input.actorId;
@@ -27,6 +43,8 @@ export function buildAuthSubject(input) {
27
43
  return {
28
44
  selfAid: input.selfAid,
29
45
  actorId,
46
+ requestId: input.requestId,
47
+ sessionId: input.sessionId,
30
48
  channel: input.channel,
31
49
  channelType: input.channelType,
32
50
  channelId: input.channelId,
@@ -41,6 +59,8 @@ export function buildAuthSubject(input) {
41
59
  allowAccess: isDaemonOwner || (suppliedRole
42
60
  ? checkRoleAccess(suppliedRole, input.selfAid)
43
61
  : roleDetail.allowAccess && checkRoleAccess(role, input.selfAid)),
62
+ permissionMode: input.permissionMode
63
+ ?? resolveRuntimePermissionMode({ selfAid: input.selfAid, role }).effectiveValue,
44
64
  };
45
65
  }
46
66
  function isProcessOwner(input) {
@@ -144,6 +164,15 @@ function resolveRoleDetail(input) {
144
164
  function auditDecision(params, decision) {
145
165
  auditCommandAuthorization({
146
166
  ts: Date.now(),
167
+ ...params.auditMetadata,
168
+ requestId: params.auditMetadata?.requestId ?? params.subject.requestId,
169
+ sessionId: params.auditMetadata?.sessionId ?? params.subject.sessionId,
170
+ agentAid: params.auditMetadata?.agentAid ?? params.subject.selfAid,
171
+ permissionMode: params.auditMetadata?.permissionMode
172
+ ?? (decision.allow ? decision.permissionMode ?? params.subject.permissionMode : params.subject.permissionMode),
173
+ toolName: params.auditMetadata?.toolName,
174
+ policyCode: params.auditMetadata?.policyCode ?? (decision.allow ? undefined : decision.code),
175
+ decisionSource: params.auditMetadata?.decisionSource ?? (decision.allow ? 'command' : 'policy'),
147
176
  source: params.source,
148
177
  operation: params.intent.operation,
149
178
  scope: params.intent.scope,
@@ -161,6 +190,5 @@ function auditDecision(params, decision) {
161
190
  reason: decision.allow ? undefined : decision.reason,
162
191
  matchedRule: decision.matchedRule,
163
192
  argsSummary: params.intent.args,
164
- ...params.auditMetadata,
165
193
  });
166
194
  }
@@ -17,6 +17,13 @@ export function auditCommandAuthorization(event) {
17
17
  function buildAuditRecord(event) {
18
18
  return {
19
19
  ts: event.ts,
20
+ requestId: event.requestId,
21
+ sessionId: event.sessionId,
22
+ agentAid: redactIdentifier(event.agentAid ?? event.selfAid),
23
+ permissionMode: event.permissionMode,
24
+ toolName: event.toolName,
25
+ policyCode: event.policyCode ?? event.code,
26
+ decisionSource: event.decisionSource,
20
27
  source: event.source,
21
28
  operation: event.operation,
22
29
  scope: event.scope,
@@ -71,6 +78,13 @@ function logAuditEvent(record) {
71
78
  `actor=${record.actorId || 'unknown'}`,
72
79
  record.taskId ? `task=${record.taskId}` : null,
73
80
  record.messageId ? `message=${record.messageId}` : null,
81
+ record.requestId ? `request=${record.requestId}` : null,
82
+ record.sessionId ? `session=${record.sessionId}` : null,
83
+ record.agentAid ? `agent=${record.agentAid}` : null,
84
+ record.permissionMode ? `permissionMode=${record.permissionMode}` : null,
85
+ record.toolName ? `tool=${record.toolName}` : null,
86
+ record.policyCode ? `policy=${record.policyCode}` : null,
87
+ record.decisionSource ? `decisionSource=${record.decisionSource}` : null,
74
88
  record.code ? `code=${record.code}` : null,
75
89
  record.matchedRule ? `rule=${record.matchedRule}` : null,
76
90
  record.dangerous ? 'dangerous=true' : null,
@@ -78,6 +92,7 @@ function logAuditEvent(record) {
78
92
  .filter(Boolean)
79
93
  .join(' ');
80
94
  logger[level](message);
95
+ logger[level](`[CommandAuditRecord] ${JSON.stringify(record)}`);
81
96
  if (record.reason)
82
97
  logger[level](` reason: ${record.reason}`);
83
98
  if (record.argvHash)
@@ -170,13 +170,13 @@ function authorizeCommandInternal(ctx, resolvedConfigCommand) {
170
170
  dangerous: opMeta.dangerous,
171
171
  matchedRule,
172
172
  constraints: permission.constraints,
173
+ permissionMode: permission.permissionMode,
173
174
  };
174
175
  }
175
176
  function matchCommandPermission(operation, category, commandPerms, isDangerous) {
176
- const namespace = operation.split('.')[0];
177
177
  const matches = [];
178
178
  for (const [rule, permission] of Object.entries(commandPerms)) {
179
- const rank = getRuleRank(rule, permission, operation, namespace, category, isDangerous);
179
+ const rank = getRuleRank(rule, permission, operation, category, isDangerous);
180
180
  if (rank > 0) {
181
181
  matches.push({ permission, matchedRule: rule, rank });
182
182
  }
@@ -186,12 +186,17 @@ function matchCommandPermission(operation, category, commandPerms, isDangerous)
186
186
  matches.sort((a, b) => b.rank - a.rank);
187
187
  return matches[0];
188
188
  }
189
- function getRuleRank(rule, permission, operation, namespace, category, isDangerous) {
189
+ function getRuleRank(rule, permission, operation, category, isDangerous) {
190
190
  const denyOffset = permission.allow ? 0 : 1;
191
191
  if (rule === operation)
192
192
  return 1000 + denyOffset;
193
- if (rule === `${namespace}.*`)
194
- return 800 + denyOffset;
193
+ if (rule.endsWith('.*')) {
194
+ const prefix = rule.slice(0, -1);
195
+ if (operation.startsWith(prefix)) {
196
+ const depth = rule.slice(0, -2).split('.').length;
197
+ return 800 + Math.min((depth - 1) * 20, 180) + denyOffset;
198
+ }
199
+ }
195
200
  if (rule.startsWith('category:') && rule.slice('category:'.length) === category)
196
201
  return 600 + denyOffset;
197
202
  if (rule === 'dangerous:*' && isDangerous)
@@ -230,7 +235,7 @@ function checkConstraints(ctx, constraints, resolvedConfigCommand) {
230
235
  const sameGroup = constraints.currentRelationOnly ? ' and may only target that same group' : '';
231
236
  return {
232
237
  ok: false,
233
- reason: `Denied ${ctx.intent.operation}: role=${ctx.role}, source_chat=${ctx.chatType ?? 'unknown'}, target_group=${target}. This role may send group messages only from a group-chat task${sameGroup}; the target being a group is not sufficient.`,
238
+ reason: `Denied ${ctx.intent.operation}: role=${ctx.role}, source_chat=${ctx.chatType ?? 'unknown'}, target_group=${target}. This role may access group operations only from a group-chat task${sameGroup}; the target being a group is not sufficient.`,
234
239
  };
235
240
  }
236
241
  if (constraints.requireDaemonOwner && !ctx.isDaemonOwner) {
@@ -401,7 +406,7 @@ function checkCurrentRelation(ctx, resolvedConfigCommand) {
401
406
  const targetGroup = stringArg(ctx.intent.args.targetId) ?? peerIdFromArg(requestedPeer);
402
407
  return {
403
408
  ok: false,
404
- reason: `Denied ${ctx.intent.operation}: role=${ctx.role}, current_group=${currentGroup}, target_group=${targetGroup}. This role may send only to the current group; cross-group sends require admin or owner permission.`,
409
+ reason: `Denied ${ctx.intent.operation}: role=${ctx.role}, current_group=${currentGroup}, target_group=${targetGroup}. This role may access only the current group; cross-group access requires admin or owner permission.`,
405
410
  };
406
411
  }
407
412
  return { ok: false, reason: 'Only the current relation can be targeted' };
@@ -498,6 +503,25 @@ function checkOwnPeer(ctx) {
498
503
  return { ok: true };
499
504
  const requestedPeerId = argPeer ? peerIdFromArg(argPeer) : undefined;
500
505
  const requestedPeerKeyId = argPeerKey ? peerIdFromArg(argPeerKey) : undefined;
506
+ if (ctx.chatType === 'group') {
507
+ if (!expectedPeerKey)
508
+ return { ok: false, reason: 'Cannot verify peer ownership' };
509
+ const candidateKeys = new Set();
510
+ if (argPeerKey)
511
+ candidateKeys.add(argPeerKey);
512
+ if (argPeer) {
513
+ candidateKeys.add(argPeer);
514
+ const channelType = parseChannelType(expectedPeerKey);
515
+ if (channelType)
516
+ candidateKeys.add(formatPeerKey(channelType, argPeer));
517
+ const legacyChannelType = parseLegacyChannelType(expectedPeerKey);
518
+ if (legacyChannelType)
519
+ candidateKeys.add(`${legacyChannelType}::${argPeer}`);
520
+ }
521
+ return candidateKeys.has(expectedPeerKey)
522
+ ? { ok: true }
523
+ : { ok: false, reason: 'Only the current group relation can be targeted' };
524
+ }
501
525
  if (ctx.actorId) {
502
526
  if (requestedPeerId && requestedPeerId !== ctx.actorId) {
503
527
  return { ok: false, reason: 'Only the actor peer can be targeted' };