evolcore 0.0.26 → 0.0.27

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 (169) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/bin/codex-managed-acl.ps1 +43 -0
  3. package/dist/agents/baseagent-availability.js +68 -0
  4. package/dist/agents/claude-runner.js +123 -22
  5. package/dist/agents/codex/app-server-client.js +63 -26
  6. package/dist/agents/codex-runner.js +316 -411
  7. package/dist/agents/ecagent-runner.js +170 -25
  8. package/dist/agents/gemini-runner.js +6 -0
  9. package/dist/agents/runner-types.js +50 -2
  10. package/dist/aun/aid/agentmd.js +73 -31
  11. package/dist/aun/aid/client.js +1 -1
  12. package/dist/aun/aid/control-aid.js +180 -1
  13. package/dist/aun/aid/identity.js +3 -3
  14. package/dist/aun/group/history.js +303 -0
  15. package/dist/aun/group/identity.js +43 -0
  16. package/dist/aun/group/index.js +5 -0
  17. package/dist/aun/{msg/group.js → group/operations.js} +161 -38
  18. package/dist/aun/{menu-client.js → menu/client.js} +12 -7
  19. package/dist/aun/menu/index.js +2 -0
  20. package/dist/aun/menu/transport.js +49 -0
  21. package/dist/aun/msg/index.js +2 -1
  22. package/dist/aun/msg/managed-operation.js +84 -18
  23. package/dist/aun/msg/mention-schema.js +51 -0
  24. package/dist/aun/msg/p2p.js +13 -8
  25. package/dist/aun/msg/upload.js +43 -88
  26. package/dist/aun/outbox.js +29 -7
  27. package/dist/aun/rpc/client.js +1 -0
  28. package/dist/aun/storage/client.js +39 -50
  29. package/dist/channels/aun.js +522 -233
  30. package/dist/channels/daemon.js +10 -6
  31. package/dist/cli/agent-command.js +108 -6
  32. package/dist/cli/agent-create.js +515 -2
  33. package/dist/cli/agent.js +109 -8
  34. package/dist/cli/aun-commands.js +191 -42
  35. package/dist/cli/config.js +8 -5
  36. package/dist/cli/contact.js +2 -2
  37. package/dist/cli/ctl-command.js +2 -2
  38. package/dist/cli/daemon-commands.js +35 -23
  39. package/dist/cli/diag.js +2 -2
  40. package/dist/cli/fs-command.js +3 -3
  41. package/dist/cli/handoff-command.js +6 -6
  42. package/dist/cli/index.js +8 -2
  43. package/dist/cli/init.js +34 -1
  44. package/dist/cli/managed-command-guard.js +2 -1
  45. package/dist/cli/menu-client.js +9 -5
  46. package/dist/cli/model.js +3 -3
  47. package/dist/cli/queue-command.js +2 -2
  48. package/dist/cli/response.js +2 -2
  49. package/dist/cli/restart-monitor.js +16 -2
  50. package/dist/cli/task-command.js +59 -0
  51. package/dist/cli/task-context.js +97 -49
  52. package/dist/cli/trigger-command.js +20 -10
  53. package/dist/config/builtin-roles.js +10 -0
  54. package/dist/config/config-field-policy.js +11 -0
  55. package/dist/config/config-manager.js +32 -4
  56. package/dist/config/config-operation-service.js +20 -5
  57. package/dist/config/contact-book-store.js +1 -1
  58. package/dist/config/contact-book.js +1 -1
  59. package/dist/config/contact-operation-service.js +1 -1
  60. package/dist/config/peer-role-resolver.js +1 -1
  61. package/dist/config/resolved-config-op.js +3 -0
  62. package/dist/config-store.js +1 -1
  63. package/dist/core/agent-application-service.js +5 -0
  64. package/dist/core/audit/event-key.js +17 -2
  65. package/dist/core/audit/log-integrity.js +34 -8
  66. package/dist/core/auth/agent-delegation.js +48 -14
  67. package/dist/core/auth/auth-gateway.js +2 -0
  68. package/dist/core/auth/authorization-audit.js +34 -38
  69. package/dist/core/auth/operation-authorizer.js +67 -5
  70. package/dist/core/auth/operation-catalog.js +110 -0
  71. package/dist/core/auth/remote-access-store.js +115 -9
  72. package/dist/core/bootstrap-messages.js +66 -7
  73. package/dist/core/bootstrap-service.js +110 -19
  74. package/dist/core/command/agent-control.js +88 -7
  75. package/dist/core/command/agent-storage-menu.js +443 -0
  76. package/dist/core/command/cli-intent-parser.js +19 -0
  77. package/dist/core/command/command-handler.js +76 -39
  78. package/dist/core/command/data-root-resolver.js +273 -0
  79. package/dist/core/command/group-menu.js +7 -2
  80. package/dist/core/command/menu-catalog.js +34 -4
  81. package/dist/core/command/menu-handler.js +348 -77
  82. package/dist/core/command/menu-protocol.js +40 -6
  83. package/dist/core/command/role-menu.js +2 -2
  84. package/dist/core/command/slash-handler.js +37 -22
  85. package/dist/core/event-catalog.js +4 -0
  86. package/dist/core/evolagent-registry.js +28 -2
  87. package/dist/core/group/invite-service.js +924 -0
  88. package/dist/core/interaction-router.js +22 -8
  89. package/dist/core/message/im-renderer.js +76 -15
  90. package/dist/core/message/message-bridge.js +94 -11
  91. package/dist/core/message/message-log.js +5 -3
  92. package/dist/core/message/message-queue.js +73 -41
  93. package/dist/core/message/response-engine.js +709 -117
  94. package/dist/core/permission/approval-gateway.js +36 -3
  95. package/dist/core/permission/ec-command-parser.js +61 -29
  96. package/dist/core/permission/protected-paths.js +134 -16
  97. package/dist/core/permission/sandbox-runtime.js +72 -3
  98. package/dist/core/permission/tool-error-code.js +15 -0
  99. package/dist/core/permission/tool-policy.js +7 -7
  100. package/dist/core/relation/peer-identity.js +57 -6
  101. package/dist/core/relation/shared-data.js +106 -0
  102. package/dist/core/role/runtime-policy.js +64 -0
  103. package/dist/core/runtime-lock.js +10 -26
  104. package/dist/core/session/runner-task-history.js +170 -0
  105. package/dist/core/session/session-manager.js +34 -6
  106. package/dist/core/session/session-mapper.js +41 -0
  107. package/dist/core/session/session-turn-coordinator.js +37 -7
  108. package/dist/core/session/session-turns.js +48 -7
  109. package/dist/core/task/task-recovery-service.js +176 -0
  110. package/dist/core/task/task-resume-operation.js +32 -0
  111. package/dist/eck/group-rules-sync.js +5 -3
  112. package/dist/eck/kit-renderer.js +1 -0
  113. package/dist/index.js +308 -54
  114. package/dist/ipc.js +141 -15
  115. package/dist/paths.js +17 -4
  116. package/dist/product.js +2 -0
  117. package/dist/trigger/event-source.js +19 -0
  118. package/dist/trigger/feedback.js +233 -40
  119. package/dist/trigger/parser.js +55 -5
  120. package/dist/trigger/patch.js +9 -0
  121. package/dist/trigger/scheduler.js +30 -4
  122. package/dist/trigger/validation.js +20 -3
  123. package/dist/utils/ecweb-utils.js +1 -0
  124. package/dist/utils/error-utils.js +40 -2
  125. package/dist/utils/logger.js +23 -13
  126. package/dist/utils/tool-summary.js +59 -3
  127. package/dist/utils/welcome.js +174 -1
  128. package/ecagent/dist/harness/agent-harness.js +3 -0
  129. package/ecagent/dist/harness/session/jsonl-storage.js +21 -2
  130. package/kits/docs/INDEX.md +0 -6
  131. package/kits/docs/channels/aun.md +15 -2
  132. package/kits/docs/channels/feishu.md +1 -1
  133. package/kits/docs/context-assembly.md +29 -17
  134. package/kits/docs/evolcore/INDEX.md +8 -3
  135. package/kits/docs/evolcore/agent-create.md +39 -7
  136. package/kits/docs/evolcore/agent.md +33 -1
  137. package/kits/docs/evolcore/aid.md +9 -1
  138. package/kits/docs/evolcore/config.md +40 -1
  139. package/kits/docs/evolcore/group-collaboration.md +1 -1
  140. package/kits/docs/evolcore/group.md +117 -2
  141. package/kits/docs/evolcore/menu-client.md +16 -0
  142. package/kits/docs/evolcore/msg.md +3 -0
  143. package/kits/docs/evolcore/storage.md +3 -0
  144. package/kits/docs/evolcore/task.md +14 -0
  145. package/kits/docs/evolcore/trigger.md +51 -3
  146. package/kits/docs/venues/group.md +5 -0
  147. package/kits/eck_manifest.json +27 -0
  148. package/kits/rules/01-overview.md +11 -7
  149. package/kits/rules/04-relation.md +2 -1
  150. package/kits/rules/05-venue.md +1 -1
  151. package/kits/rules/06-channel.md +6 -4
  152. package/kits/schemas/agent-config.schema.12.json +25 -1
  153. package/kits/schemas/daemon.schema.6.json +2 -2
  154. package/kits/schemas/defaults.schema.5.json +1 -1
  155. package/kits/schemas/relation-config.schema.1.json +1 -1
  156. package/kits/schemas/relation-config.schema.5.json +1 -1
  157. package/kits/schemas/role-config.schema.1.json +14 -0
  158. package/kits/templates/roles/member.json +5 -0
  159. package/kits/templates/roles/visitor.json +5 -0
  160. package/kits/templates/system-fragments/baseagent.md +1 -1
  161. package/kits/templates/system-fragments/bootstrap.md +26 -9
  162. package/kits/templates/system-fragments/channel.md +8 -44
  163. package/kits/templates/system-fragments/commands.md +10 -2
  164. package/kits/templates/system-fragments/group-unread-history.md +7 -0
  165. package/kits/templates/system-fragments/reply.md +56 -0
  166. package/package.json +7 -3
  167. package/dist/aun/group-identity.js +0 -9
  168. /package/dist/aun/{group-fs-download.js → group/fs-download.js} +0 -0
  169. /package/dist/aun/{msg/group-index.js → group/index-rpc.js} +0 -0
@@ -25,6 +25,7 @@ const GROUP_MANAGEMENT_OPERATIONS = [
25
25
  ['ec.group.list', 'read', ['agent'], 'List groups joined by the current Agent'],
26
26
  ['ec.group.info', 'read', ['relation', 'agent'], 'Read group information'],
27
27
  ['ec.group.pull', 'read', ['relation', 'agent'], 'Pull group messages'],
28
+ ['ec.group.history', 'read', ['relation', 'agent'], 'Read local group conversation history'],
28
29
  ['ec.group.members', 'read', ['relation', 'agent'], 'List group members'],
29
30
  ['ec.group.online', 'read', ['relation', 'agent'], 'List online group members'],
30
31
  ['ec.group.rules.get', 'read', ['relation', 'agent'], 'Read the published group rules file'],
@@ -47,6 +48,11 @@ const GROUP_MANAGEMENT_OPERATIONS = [
47
48
  ['ec.group.owner', 'write-agent', ['agent'], 'Transfer group ownership'],
48
49
  ['ec.group.ban', 'write-agent', ['agent'], 'Read or update the group ban list'],
49
50
  ['ec.group.unban', 'write-agent', ['agent'], 'Remove a group ban'],
51
+ ['ec.group.block', 'write-own', ['relation', 'agent'], 'Block group message reception'],
52
+ ['ec.group.unblock', 'write-own', ['relation', 'agent'], 'Unblock group message reception'],
53
+ ['ec.group.blocklist', 'read', ['agent'], 'Read the group block list'],
54
+ ['ec.group.report-status', 'write-own', ['relation', 'agent'], 'Report the current Agent group status'],
55
+ ['ec.group.member-statuses', 'read', ['relation', 'agent'], 'Read reported group member statuses'],
50
56
  ['ec.group.rules.set', 'write-agent', ['agent'], 'Upload and publish group rules content'],
51
57
  ['ec.group.rules.publish', 'write-agent', ['agent'], 'Publish the current group rules file'],
52
58
  ['ec.group.rules.update', 'write-agent', ['agent'], 'Update group join rules'],
@@ -365,6 +371,86 @@ const OPERATIONS = [
365
371
  description: '获取文件内容(需路径沙箱检查)',
366
372
  sources: ['slash', 'menu'],
367
373
  },
374
+ {
375
+ id: 'file.root.agent',
376
+ category: 'read',
377
+ dangerous: false,
378
+ defaultScopes: ['agent'],
379
+ description: '获取 Agent 内部数据根目录句柄',
380
+ sources: ['menu'],
381
+ },
382
+ {
383
+ id: 'file.root.relation',
384
+ category: 'read',
385
+ dangerous: false,
386
+ defaultScopes: ['relation', 'agent'],
387
+ description: '获取当前或 owner 指定关系的共享数据根目录句柄',
388
+ sources: ['menu'],
389
+ },
390
+ {
391
+ id: 'file.data.list',
392
+ category: 'read',
393
+ dangerous: false,
394
+ defaultScopes: ['relation', 'agent'],
395
+ description: '列出已授权内部数据根目录',
396
+ sources: ['menu'],
397
+ },
398
+ {
399
+ id: 'file.data.query',
400
+ category: 'read',
401
+ dangerous: false,
402
+ defaultScopes: ['relation', 'agent'],
403
+ description: '查询已授权内部数据根文件元信息',
404
+ sources: ['menu'],
405
+ },
406
+ {
407
+ id: 'file.data.fetch',
408
+ category: 'read',
409
+ dangerous: false,
410
+ defaultScopes: ['relation', 'agent'],
411
+ description: '下载已授权内部数据根文件',
412
+ sources: ['menu'],
413
+ },
414
+ {
415
+ id: 'file.root.storage',
416
+ category: 'read',
417
+ dangerous: false,
418
+ defaultScopes: ['agent'],
419
+ description: '获取 Agent 自有云存储根句柄',
420
+ sources: ['menu'],
421
+ },
422
+ {
423
+ id: 'file.storage.list',
424
+ category: 'read',
425
+ dangerous: false,
426
+ defaultScopes: ['agent'],
427
+ description: '列出 Agent 自有云存储目录',
428
+ sources: ['menu'],
429
+ },
430
+ {
431
+ id: 'file.storage.query',
432
+ category: 'read',
433
+ dangerous: false,
434
+ defaultScopes: ['agent'],
435
+ description: '查询 Agent 自有云存储元信息',
436
+ sources: ['menu'],
437
+ },
438
+ {
439
+ id: 'file.storage.fetch',
440
+ category: 'read',
441
+ dangerous: false,
442
+ defaultScopes: ['agent'],
443
+ description: '获取 Agent 自有云存储短时下载链接',
444
+ sources: ['menu'],
445
+ },
446
+ {
447
+ id: 'file.storage.quota',
448
+ category: 'read',
449
+ dangerous: false,
450
+ defaultScopes: ['agent'],
451
+ description: '查询 Agent 自有云存储配额',
452
+ sources: ['menu'],
453
+ },
368
454
  // ── Trigger Operations ──
369
455
  {
370
456
  id: 'trigger.list',
@@ -624,6 +710,14 @@ const OPERATIONS = [
624
710
  description: '读取或更新当前 Agent 的 persona.md',
625
711
  sources: ['agent-tool', 'control'],
626
712
  },
713
+ {
714
+ id: 'agent.welcome',
715
+ category: 'write-agent',
716
+ dangerous: false,
717
+ defaultScopes: ['agent'],
718
+ description: '读取或更新当前 Agent 的 Bootstrap welcome.md',
719
+ sources: ['agent-tool', 'control'],
720
+ },
627
721
  {
628
722
  id: 'agent.create',
629
723
  category: 'process',
@@ -632,6 +726,14 @@ const OPERATIONS = [
632
726
  description: '创建新 agent',
633
727
  sources: ['menu', 'menu.cli', 'agent-tool', 'control'],
634
728
  },
729
+ {
730
+ id: 'agent.initialize',
731
+ category: 'process',
732
+ dangerous: true,
733
+ defaultScopes: ['control'],
734
+ description: 'Create and initialize an Agent from an explicit final plan',
735
+ sources: ['menu', 'menu.cli', 'agent-tool', 'control'],
736
+ },
635
737
  {
636
738
  id: 'agent.enable',
637
739
  category: 'process',
@@ -935,6 +1037,14 @@ const OPERATIONS = [
935
1037
  ...HANDOFF_READ_OPERATIONS,
936
1038
  ...HANDOFF_WRITE_OPERATIONS,
937
1039
  ...QUEUE_OPERATIONS,
1040
+ {
1041
+ id: 'ec.task.resume',
1042
+ category: 'write-agent',
1043
+ dangerous: false,
1044
+ defaultScopes: ['agent'],
1045
+ description: 'Resume a paused or abnormally interrupted Agent session task',
1046
+ sources: ['agent-tool', 'control'],
1047
+ },
938
1048
  {
939
1049
  id: 'ec.version',
940
1050
  category: 'read',
@@ -12,13 +12,21 @@ export function remoteAccessTokenStorePath() {
12
12
  function loadStore() {
13
13
  try {
14
14
  const value = JSON.parse(fs.readFileSync(remoteAccessTokenStorePath(), 'utf8'));
15
- if (Array.isArray(value.tokens))
16
- return value;
15
+ if (Array.isArray(value.tokens)) {
16
+ return {
17
+ tokens: value.tokens,
18
+ pairingIssuances: Array.isArray(value.pairingIssuances) ? value.pairingIssuances : [],
19
+ };
20
+ }
17
21
  }
18
22
  catch { }
19
- return { tokens: [] };
23
+ return { tokens: [], pairingIssuances: [] };
20
24
  }
21
25
  function saveStore(store) {
26
+ // Issuance history contains no credentials. Keep 30 days; live tokens retain
27
+ // their own provenance even after the corresponding history is pruned.
28
+ const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000;
29
+ store.pairingIssuances = store.pairingIssuances.filter(record => record.expiresAt >= cutoff);
22
30
  const file = remoteAccessTokenStorePath();
23
31
  fs.mkdirSync(path.dirname(file), { recursive: true });
24
32
  const temporary = `${file}.${process.pid}.tmp`;
@@ -87,6 +95,12 @@ export class RemoteAccessService {
87
95
  pairingCode = '';
88
96
  pairingExpiresAt = 0;
89
97
  pairingAuthorizedBy;
98
+ pairingId = '';
99
+ pairingIssuedAt = 0;
100
+ pairingIssuers = [];
101
+ // Do not recycle consumed codes during their original validity window,
102
+ // including when other sources have issued intervening codes.
103
+ recentPairingCodes = new Map();
90
104
  /** Synchronous reservation: concurrent invitations cannot issue two grants. */
91
105
  issueDiagnosticToken(controlAid, agentAid, ownerAid, now = Date.now()) {
92
106
  const store = loadStore();
@@ -124,18 +138,62 @@ export class RemoteAccessService {
124
138
  saveStore(store);
125
139
  return record;
126
140
  }
127
- getPairingCode(authorizedBy, now = Date.now()) {
128
- if (!this.pairingCode || now >= this.pairingExpiresAt) {
129
- this.pairingCode = String(crypto.randomInt(0, 1_000_000)).padStart(6, '0');
141
+ getPairingCode(authorizedBy, now = Date.now(), source = 'host-cli') {
142
+ const owner = validOwner(authorizedBy);
143
+ if (!['host-cli', 'pair', 'menu', 'ecweb-local'].includes(source)
144
+ || ((source === 'pair' || source === 'menu') && !owner)) {
145
+ throw new Error('pairing source requires a valid source and an owner for /pair or Menu');
146
+ }
147
+ const generated = !this.pairingCode || now >= this.pairingExpiresAt;
148
+ if (generated) {
149
+ for (const [code, expiresAt] of this.recentPairingCodes) {
150
+ if (now >= expiresAt)
151
+ this.recentPairingCodes.delete(code);
152
+ }
153
+ // Choose uniformly from unused values without an unbounded retry loop.
154
+ let code = crypto.randomInt(0, 1_000_000 - this.recentPairingCodes.size);
155
+ for (const occupied of [...this.recentPairingCodes.keys()].sort((a, b) => a - b)) {
156
+ if (occupied > code)
157
+ break;
158
+ code++;
159
+ }
160
+ this.pairingCode = String(code).padStart(6, '0');
161
+ this.pairingId = crypto.randomUUID();
162
+ this.pairingIssuedAt = now;
130
163
  this.pairingExpiresAt = now + REMOTE_ACCESS_PAIRING_TTL_MS;
164
+ this.recentPairingCodes.set(code, this.pairingExpiresAt);
131
165
  this.pairingAuthorizedBy = undefined;
166
+ this.pairingIssuers = [];
132
167
  }
133
- const owner = validOwner(authorizedBy);
168
+ const previousOwner = this.pairingAuthorizedBy;
134
169
  if (!this.pairingAuthorizedBy && owner)
135
170
  this.pairingAuthorizedBy = owner;
171
+ const issuer = {
172
+ source,
173
+ issuedAt: now,
174
+ ...((source === 'pair' || source === 'menu') && owner ? { authorizedBy: owner } : {}),
175
+ };
176
+ const addedIssuer = !this.pairingIssuers.some(candidate => candidate.source === issuer.source
177
+ && candidate.authorizedBy === issuer.authorizedBy);
178
+ if (addedIssuer) {
179
+ this.pairingIssuers.push(issuer);
180
+ }
181
+ try {
182
+ this.recordPairingIssuance();
183
+ }
184
+ catch (error) {
185
+ if (generated)
186
+ this.clearPairingCode();
187
+ else {
188
+ this.pairingAuthorizedBy = previousOwner;
189
+ if (addedIssuer)
190
+ this.pairingIssuers.pop();
191
+ }
192
+ throw error;
193
+ }
136
194
  return { code: this.pairingCode, expiresAt: this.pairingExpiresAt };
137
195
  }
138
- redeemPairingCode(code, label, authorizedBy, now = Date.now()) {
196
+ redeemPairingCode(code, label, now = Date.now()) {
139
197
  if (typeof code !== 'string' || !/^\d{6}$/.test(code)) {
140
198
  return { ok: false, code: 'INVALID_ARGUMENT', error: 'pairing code must be six digits' };
141
199
  }
@@ -149,17 +207,65 @@ export class RemoteAccessService {
149
207
  const token = crypto.randomBytes(32).toString('hex');
150
208
  const store = loadStore();
151
209
  pruneExpired(store, now);
152
- const owner = validOwner(authorizedBy) ?? this.pairingAuthorizedBy;
210
+ const owner = this.pairingAuthorizedBy;
211
+ const pairingId = this.pairingId;
212
+ const pairingIssuers = this.pairingIssuers.map(issuer => ({ ...issuer }));
153
213
  store.tokens.push({
154
214
  token,
155
215
  createdAt,
156
216
  lastActive: createdAt,
157
217
  label: validLabel(label),
158
218
  ...(owner ? { authorizedBy: owner } : {}),
219
+ pairingId,
220
+ pairingIssuers,
159
221
  });
222
+ const issuance = store.pairingIssuances.find(record => record.pairingId === pairingId);
223
+ if (issuance) {
224
+ issuance.redeemedAt = createdAt;
225
+ }
226
+ else {
227
+ store.pairingIssuances.push({
228
+ pairingId,
229
+ issuedAt: this.pairingIssuedAt,
230
+ expiresAt: this.pairingExpiresAt,
231
+ issuers: pairingIssuers,
232
+ redeemedAt: createdAt,
233
+ });
234
+ }
160
235
  saveStore(store);
236
+ // A pairing code is a one-time capability. Clear it only after the token
237
+ // and provenance record have been atomically saved. No await separates
238
+ // validation, persistence and consumption, so concurrent IPC calls cannot
239
+ // grant the same code twice.
240
+ this.clearPairingCode();
161
241
  return { ok: true, token, expiresAt: createdAt + REMOTE_ACCESS_TOKEN_TTL_MS };
162
242
  }
243
+ clearPairingCode() {
244
+ this.pairingCode = '';
245
+ this.pairingExpiresAt = 0;
246
+ this.pairingAuthorizedBy = undefined;
247
+ this.pairingId = '';
248
+ this.pairingIssuedAt = 0;
249
+ this.pairingIssuers = [];
250
+ }
251
+ recordPairingIssuance() {
252
+ if (!this.pairingId || !this.pairingCode)
253
+ return;
254
+ const store = loadStore();
255
+ const existing = store.pairingIssuances.find(record => record.pairingId === this.pairingId);
256
+ const record = {
257
+ pairingId: this.pairingId,
258
+ issuedAt: this.pairingIssuedAt,
259
+ expiresAt: this.pairingExpiresAt,
260
+ issuers: this.pairingIssuers.map(issuer => ({ ...issuer })),
261
+ ...(existing?.redeemedAt ? { redeemedAt: existing.redeemedAt } : {}),
262
+ };
263
+ if (existing)
264
+ Object.assign(existing, record);
265
+ else
266
+ store.pairingIssuances.push(record);
267
+ saveStore(store);
268
+ }
163
269
  issueLocalToken(label, now = Date.now()) {
164
270
  const token = crypto.randomBytes(32).toString('hex');
165
271
  const store = loadStore();
@@ -1,10 +1,12 @@
1
1
  import fs from 'fs';
2
+ import { loadAgent } from '../config-store.js';
3
+ import { resolveAgentLifecycle } from '../config/lifecycle.js';
2
4
  import { agentMdPath, resolvePaths } from '../paths.js';
3
5
  import { parseAgentDisplayName, resolveAgentDisplayName } from '../aun/aid/agentmd.js';
4
- import { generateWelcomeMessage } from '../utils/welcome.js';
6
+ import { generateWelcomeMessage, parseAgentWelcomeMetadata, readAgentWelcomeTemplate, renderPostBootstrapWelcomeMessage, } from '../utils/welcome.js';
5
7
  import * as outbox from '../aun/outbox.js';
6
8
  import { chatDirPath } from './session/session-fs-store.js';
7
- import { hasMessageLogOperation } from './message/message-log.js';
9
+ import { hasMessageLogOperation, findMessageLogOperationMessageId } from './message/message-log.js';
8
10
  import { isDeliveryTarget, sameDeliveryTarget } from './message/message-utils.js';
9
11
  export const BOOTSTRAP_MESSAGE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
10
12
  export const BOOTSTRAP_NOTIFICATION_WINDOW_MS = 24 * 60 * 60 * 1000;
@@ -32,10 +34,27 @@ export function isPostBootstrapWelcomeOperationId(operationId, aid) {
32
34
  export function renderPostBootstrapWelcome(aid, owner, ownerName) {
33
35
  const mdPath = agentMdPath(aid);
34
36
  const agentMd = fs.existsSync(mdPath) ? fs.readFileSync(mdPath, 'utf-8') : '';
35
- const agentName = parseAgentDisplayName(agentMd)
37
+ const metadata = parseAgentWelcomeMetadata(agentMd);
38
+ const agentName = metadata.name || parseAgentDisplayName(agentMd)
36
39
  || resolveAgentDisplayName(aid)
37
40
  || aid.split('.')[0];
38
41
  const fallbackOwnerName = owner.replace(/^@/, '').split('.')[0];
42
+ const template = readAgentWelcomeTemplate(aid);
43
+ if (template) {
44
+ return renderPostBootstrapWelcomeMessage({
45
+ template,
46
+ agentName,
47
+ agentDescription: metadata.description,
48
+ agentAid: aid,
49
+ agentTags: metadata.tags,
50
+ ownerName: ownerName || fallbackOwnerName,
51
+ ownerAid: owner,
52
+ });
53
+ }
54
+ const config = loadAgent(aid);
55
+ if (config && resolveAgentLifecycle(config) === 'bootstrapping' && config.initialization?.mode !== 'supplied') {
56
+ throw new Error('WELCOME_REQUIRED: Owner-confirmed welcome.md is required before activation');
57
+ }
39
58
  return generateWelcomeMessage({
40
59
  channelType: 'aun',
41
60
  agentName,
@@ -49,15 +68,18 @@ export function preparePostBootstrapWelcomeOutbox(aid, owner, ownerName, deliver
49
68
  const legacyOperationId = postBootstrapWelcomeOperationId(aid);
50
69
  if (hasMessageLogOperation(chatDir, operationId) || hasMessageLogOperation(chatDir, legacyOperationId))
51
70
  return;
52
- const legacy = outbox.findByDedupeKey(aid, legacyOperationId);
71
+ const legacy = outbox.findByDedupeKey(aid, legacyOperationId, { includeTerminal: true });
53
72
  if (legacy?.channelId === owner)
54
73
  return;
55
- const existing = outbox.findByDedupeKey(aid, operationId);
74
+ const existing = outbox.findByDedupeKey(aid, operationId, { includeTerminal: true });
56
75
  if (existing
57
76
  && existing.channelId === owner
58
77
  && isDeliveryTarget(existing.delivery)
59
78
  && sameDeliveryTarget(existing.delivery, delivery))
60
79
  return;
80
+ // Render before touching an existing route record: malformed content must not
81
+ // remove the only durable record of an earlier attempt.
82
+ const text = renderPostBootstrapWelcome(aid, owner, ownerName);
61
83
  if (existing)
62
84
  outbox.remove(aid, existing.id);
63
85
  outbox.enqueue(aid, {
@@ -66,7 +88,7 @@ export function preparePostBootstrapWelcomeOutbox(aid, owner, ownerName, deliver
66
88
  dedupeKey: operationId,
67
89
  critical: true,
68
90
  type: 'text',
69
- text: renderPostBootstrapWelcome(aid, owner, ownerName),
91
+ text,
70
92
  ttl: BOOTSTRAP_MESSAGE_TTL_MS,
71
93
  context: {
72
94
  delivery,
@@ -79,6 +101,43 @@ export function preparePostBootstrapWelcomeOutbox(aid, owner, ownerName, deliver
79
101
  },
80
102
  });
81
103
  }
104
+ /** Read durable evidence, including failures and already accepted operations. */
105
+ export function postBootstrapWelcomeStatus(aid, owners) {
106
+ const entries = findPostBootstrapWelcomeOutbox(aid, { includeTerminal: true });
107
+ return {
108
+ deliveries: Array.from(new Set(owners)).map(owner => {
109
+ const operationId = postBootstrapWelcomeOperationId(aid, owner);
110
+ const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', owner, aid);
111
+ const loggedId = findMessageLogOperationMessageId(chatDir, operationId)
112
+ ?? findMessageLogOperationMessageId(chatDir, postBootstrapWelcomeOperationId(aid));
113
+ const entry = entries.find(candidate => candidate.channelId === owner
114
+ && candidate.delivery?.chatType === 'private'
115
+ && (candidate.dedupeKey === operationId || candidate.dedupeKey === postBootstrapWelcomeOperationId(aid)));
116
+ const base = { owner, operationId, ...(entry ? { outboxId: entry.id } : {}) };
117
+ if (loggedId)
118
+ return { ...base, status: 'sent', messageId: loggedId };
119
+ if (!entry)
120
+ return { ...base, status: 'not_prepared' };
121
+ const state = outbox.deliveryState(aid, entry.id);
122
+ if (state.status === 'sent' || state.status === 'failed')
123
+ return { ...base, ...state };
124
+ if (state.status === 'missing')
125
+ return { ...base, status: 'not_prepared' };
126
+ return { ...base, status: 'queued', error: entry.lastError, code: entry.lastErrorCode };
127
+ }),
128
+ };
129
+ }
130
+ /** Supersede unsent setup prompts when the agent becomes active. Keep diagnostic evidence. */
131
+ export function cancelPendingBootstrapPrompts(aid) {
132
+ for (const entry of outbox.findByDedupePrefix(aid, bootstrapInitialMessageOperationPrefix(aid))) {
133
+ if (entry.deliveryReceipt)
134
+ continue;
135
+ outbox.markTerminal(aid, entry.id, {
136
+ code: 'BOOTSTRAP_SUPERSEDED',
137
+ error: 'Bootstrap is complete; the initial prompt is no longer needed',
138
+ }, entry);
139
+ }
140
+ }
82
141
  export function hasPendingPostBootstrapWelcomeOutbox(aid) {
83
142
  return findPostBootstrapWelcomeOutbox(aid)
84
143
  .some(entry => outbox.isDeliveryForChannel(entry.delivery, entry.channelId));
@@ -92,7 +151,7 @@ export function findPostBootstrapWelcomeOutbox(aid, opts = {}) {
92
151
  }
93
152
  export function pendingPostBootstrapWelcomeOwners(aid) {
94
153
  return findPostBootstrapWelcomeOutbox(aid)
95
- .filter(entry => outbox.isDeliveryForChannel(entry.delivery, entry.channelId))
154
+ .filter(entry => !entry.context?.sessionId && outbox.isDeliveryForChannel(entry.delivery, entry.channelId))
96
155
  .map(entry => entry.channelId)
97
156
  .filter((owner, index, all) => all.indexOf(owner) === index);
98
157
  }