evolcore 0.0.3 → 0.0.5

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 (148) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.md +45 -10
  3. package/bin/ec-safe-output.js +89 -24
  4. package/dist/agents/baseagent.js +46 -0
  5. package/dist/agents/claude-runner.js +1 -31
  6. package/dist/agents/codex-app-server-client.js +68 -0
  7. package/dist/agents/codex-runner.js +157 -5
  8. package/dist/agents/runner-types.js +2 -2
  9. package/dist/aun/aid/agentmd.js +79 -0
  10. package/dist/aun/aid/encryption-seed-policy.js +29 -0
  11. package/dist/aun/aid/index.js +1 -1
  12. package/dist/aun/aid/store.js +2 -5
  13. package/dist/channels/aun.js +220 -112
  14. package/dist/channels/daemon.js +18 -4
  15. package/dist/channels/dingtalk.js +52 -137
  16. package/dist/channels/feishu.js +56 -4
  17. package/dist/channels/qqbot.js +23 -1
  18. package/dist/channels/wechat.js +303 -155
  19. package/dist/channels/wecom.js +383 -7
  20. package/dist/cli/aun-commands.js +11 -11
  21. package/dist/cli/daemon-commands.js +46 -15
  22. package/dist/cli/handoff-command.js +2 -1
  23. package/dist/cli/init-channel.js +185 -67
  24. package/dist/cli/init.js +49 -27
  25. package/dist/cli/trigger-command.js +92 -11
  26. package/dist/config/access-policy-domain.js +18 -0
  27. package/dist/config/access-policy.js +110 -0
  28. package/dist/config/builtin-role-templates.js +27 -14
  29. package/dist/config/builtin-roles.js +25 -6
  30. package/dist/config/config-field-policy.js +17 -5
  31. package/dist/config/config-manager.js +198 -22
  32. package/dist/config/config-operation-service.js +35 -38
  33. package/dist/config/contact-bind-code.js +274 -0
  34. package/dist/config/contact-book-store.js +173 -5
  35. package/dist/config/contact-book.js +32 -0
  36. package/dist/config/contact-operation-service.js +9 -3
  37. package/dist/config/contact-request-service.js +331 -0
  38. package/dist/config/peer-role-resolver.js +3 -1
  39. package/dist/config/schema-registry.js +49 -24
  40. package/dist/config-store.js +14 -2
  41. package/dist/core/auth/agent-delegation.js +105 -11
  42. package/dist/core/auth/authorization-audit.js +6 -0
  43. package/dist/core/auth/operation-authorizer.js +8 -0
  44. package/dist/core/auth/operation-catalog.js +51 -3
  45. package/dist/core/auth/trigger-authorization.js +15 -0
  46. package/dist/core/bootstrap-service.js +2 -9
  47. package/dist/core/channel-loader.js +6 -2
  48. package/dist/core/command/command-handler.js +10 -13
  49. package/dist/core/command/connect-menu.js +229 -26
  50. package/dist/core/command/evol-menu-version-gate.js +38 -0
  51. package/dist/core/command/group-menu.js +421 -0
  52. package/dist/core/command/menu-handler.js +269 -85
  53. package/dist/core/command/menu-protocol.js +8 -2
  54. package/dist/core/command/menu-token-store.js +102 -0
  55. package/dist/core/command/role-menu.js +16 -0
  56. package/dist/core/command/slash-gate.js +1 -1
  57. package/dist/core/command/slash-handler.js +158 -31
  58. package/dist/core/daemon-file-cache.js +28 -0
  59. package/dist/core/event-catalog.js +29 -0
  60. package/dist/core/evolagent-registry.js +4 -39
  61. package/dist/core/handoff/runtime.js +162 -37
  62. package/dist/core/handoff/store.js +46 -2
  63. package/dist/core/handoff/types.js +1 -0
  64. package/dist/core/inference/text-inference.js +16 -74
  65. package/dist/core/message/file-markers.js +7 -0
  66. package/dist/core/message/im-renderer.js +18 -14
  67. package/dist/core/message/inbound-admission.js +134 -0
  68. package/dist/core/message/message-bridge.js +616 -74
  69. package/dist/core/message/message-log.js +1 -0
  70. package/dist/core/message/message-queue.js +185 -10
  71. package/dist/core/message/peer-mode.js +7 -8
  72. package/dist/core/message/response-engine.js +381 -79
  73. package/dist/core/permission/approval-gateway.js +17 -10
  74. package/dist/core/permission/ec-command-parser.js +101 -46
  75. package/dist/core/permission/tool-policy.js +69 -24
  76. package/dist/core/protected-paths.js +36 -11
  77. package/dist/core/session/session-fs-store.js +19 -4
  78. package/dist/core/session/session-manager.js +232 -4
  79. package/dist/core/session/session-mapper.js +2 -0
  80. package/dist/core/session/session-renew.js +125 -69
  81. package/dist/core/session/session-turn-coordinator.js +5 -1
  82. package/dist/eck/kit-renderer.js +12 -1
  83. package/dist/eck/message-renderer.js +79 -1
  84. package/dist/index.js +224 -89
  85. package/dist/ipc.js +81 -4
  86. package/dist/paths.js +3 -0
  87. package/dist/response-system/config-resolver.js +29 -0
  88. package/dist/response-system/coordinator.js +8 -32
  89. package/dist/response-system/engines/v1/proactive-flow.js +1 -1
  90. package/dist/response-system/index.js +1 -0
  91. package/dist/response-system/modes/single-session/index.js +7 -4
  92. package/dist/trigger/parser.js +55 -15
  93. package/dist/trigger/patch.js +4 -1
  94. package/dist/trigger/scheduler.js +441 -39
  95. package/dist/utils/cross-platform.js +8 -2
  96. package/dist/utils/error-dict.json +7 -0
  97. package/dist/utils/evolcore-version.js +21 -0
  98. package/dist/utils/logger.js +2 -2
  99. package/dist/utils/process-introspect.js +19 -3
  100. package/dist/utils/stable-semver.js +21 -0
  101. package/dist/utils/stats.js +33 -9
  102. package/kits/docs/channels/aun.md +4 -13
  103. package/kits/docs/evolcore/INDEX.md +1 -1
  104. package/kits/docs/evolcore/config.md +47 -2
  105. package/kits/docs/evolcore/contact.md +8 -3
  106. package/kits/docs/evolcore/group-rules.md +46 -4
  107. package/kits/docs/evolcore/group.md +4 -4
  108. package/kits/docs/evolcore/msg.md +5 -5
  109. package/kits/docs/evolcore/trigger.md +25 -8
  110. package/kits/docs/path-registry.md +36 -17
  111. package/kits/eck_message_manifest.json +12 -1
  112. package/kits/migrations/migrate-contact-book-v2.mjs +7 -0
  113. package/kits/rules/01-overview.md +17 -7
  114. package/kits/rules/02-navigation.md +38 -18
  115. package/kits/rules/03-identity.md +28 -24
  116. package/kits/rules/04-relation.md +44 -28
  117. package/kits/rules/05-venue.md +31 -15
  118. package/kits/rules/06-channel.md +11 -7
  119. package/kits/schemas/_meta.json +18 -7
  120. package/kits/schemas/agent-config.schema.7.json +303 -0
  121. package/kits/schemas/agent-config.schema.8.json +304 -0
  122. package/kits/schemas/agent-config.schema.9.json +364 -0
  123. package/kits/schemas/contact-book.schema.3.json +67 -0
  124. package/kits/schemas/daemon.schema.1.json +3 -2
  125. package/kits/schemas/daemon.schema.2.json +101 -0
  126. package/kits/schemas/daemon.schema.3.json +123 -0
  127. package/kits/schemas/defaults.schema.2.json +85 -0
  128. package/kits/schemas/defaults.schema.3.json +73 -0
  129. package/kits/schemas/relation-config.schema.6.json +46 -0
  130. package/kits/schemas/relation-config.schema.7.json +59 -0
  131. package/kits/schemas/single-session.schema.2.json +57 -0
  132. package/kits/templates/message-fragments/handoff-context-to-target.md +9 -0
  133. package/kits/templates/message-fragments/handoff-request-to-target.md +14 -8
  134. package/kits/templates/message-fragments/handoff-response-to-origin.md +5 -6
  135. package/kits/templates/roles/admin.json +46 -0
  136. package/kits/templates/roles/member.json +4 -0
  137. package/kits/templates/roles/visitor.json +4 -0
  138. package/kits/templates/system-fragments/channel.md +12 -2
  139. package/kits/templates/system-fragments/identity.md +3 -1
  140. package/kits/templates/system-fragments/relation.md +1 -1
  141. package/kits/templates/system-fragments/session.md +6 -2
  142. package/package.json +4 -2
  143. package/MIGRATION-0.5.0.md +0 -378
  144. package/ROLE_ACCESS_CONTROL.md +0 -174
  145. package/dist/channels/contact-bind-code.js +0 -134
  146. package/dist/channels/wecom-card.js +0 -101
  147. package/dist/channels/wecom-onboarding.js +0 -82
  148. package/dist/channels/wecom-state.js +0 -191
@@ -0,0 +1,331 @@
1
+ import crypto from 'crypto';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { isValidAid } from '../aun/aid/validation.js';
5
+ import { agentContactOperations } from '../paths.js';
6
+ import { listStaticAgentAdmins, listStaticAgentOwners } from './peer-role-resolver.js';
7
+ import { listAgentOwners } from './access-policy.js';
8
+ import { ContactMutationError, mutateContactBookPrepared, } from './contact-book-store.js';
9
+ export const CONTACT_REQUEST_TTL_MS = 20 * 60 * 1000;
10
+ export const CONTACT_REQUEST_WINDOW_MS = 60 * 60 * 1000;
11
+ export const CONTACT_REQUEST_MAX_PER_WINDOW = 3;
12
+ export const CONTACT_REQUEST_MAX_PENDING = 100;
13
+ export const CONTACT_REQUEST_MAX_RETAINED = 1000;
14
+ export const CONTACT_REQUEST_NOTE_MAX_LENGTH = 500;
15
+ export async function mutateContactWithOperation(input) {
16
+ const at = new Date(input.now ?? Date.now()).toISOString();
17
+ const logFile = agentContactOperations(input.selfAid);
18
+ const result = await mutateContactBookPrepared(input.selfAid, ({ contactRevision }) => {
19
+ if (input.expectedContactRevision !== undefined && input.expectedContactRevision !== contactRevision) {
20
+ throw new ContactMutationError('CONFLICT', 'Contact book changed since it was read', {
21
+ expectedContactRevision: input.expectedContactRevision,
22
+ contactRevision,
23
+ });
24
+ }
25
+ return {
26
+ mutation: input.mutation,
27
+ actor: input.actorId,
28
+ value: undefined,
29
+ beforeCommit: ({ contactRevision: nextRevision }) => appendOperationRecords(logFile, [operationRecord({
30
+ at,
31
+ op: input.operation,
32
+ primaryId: input.primaryId,
33
+ actorId: input.actorId,
34
+ contactRevision: nextRevision,
35
+ })]),
36
+ };
37
+ });
38
+ const { value: _value, ...mutationResult } = result;
39
+ return mutationResult;
40
+ }
41
+ export async function submitContactRequest(input) {
42
+ const applicantAid = String(input.applicantAid || '').trim();
43
+ const owners = listAgentOwners(input.selfAid);
44
+ if (!isValidAid(applicantAid)) {
45
+ return { code: 'invalid-applicant', contactRevision: '' };
46
+ }
47
+ if (owners.includes(applicantAid)) {
48
+ return { code: 'owner', contactRevision: '' };
49
+ }
50
+ if (owners.length === 0) {
51
+ return { code: 'no-owner', contactRevision: '' };
52
+ }
53
+ const note = normalizeNote(input.note);
54
+ if (note === null) {
55
+ return { code: 'invalid-note', contactRevision: '' };
56
+ }
57
+ const nowMs = input.now ?? Date.now();
58
+ const nowIso = new Date(nowMs).toISOString();
59
+ const logFile = agentContactOperations(input.selfAid);
60
+ const result = await mutateContactBookPrepared(input.selfAid, ({ contact }) => {
61
+ const expiration = collectExpiredMutations(contact, nowMs);
62
+ const effectiveEntry = effectiveEntryAfterExpirations(contact.contacts[applicantAid], nowMs);
63
+ if (effectiveEntry?.status === 'blocked') {
64
+ return preparedWithExpirations(expiration, { code: 'blocked', contactRevision: '' }, logFile);
65
+ }
66
+ if (effectiveEntry && (effectiveEntry.status === undefined || effectiveEntry.status === 'active')) {
67
+ return preparedWithExpirations(expiration, { code: 'already-contact', contactRevision: '' }, logFile);
68
+ }
69
+ const operations = readOperationLog(logFile);
70
+ const windowStart = nowMs - CONTACT_REQUEST_WINDOW_MS;
71
+ const recentCount = operations.filter(record => (record.primaryId === applicantAid
72
+ && (record.op === 'request' || record.op === 'resubmit')
73
+ && Date.parse(record.at) > windowStart
74
+ && Date.parse(record.at) <= nowMs)).length;
75
+ if (recentCount >= CONTACT_REQUEST_MAX_PER_WINDOW) {
76
+ return preparedWithExpirations(expiration, { code: 'rate-limited', contactRevision: '' }, logFile);
77
+ }
78
+ const counts = countRequestRecordsAfterExpirations(contact, nowMs);
79
+ const isExistingPending = effectiveEntry?.status === 'pending';
80
+ if (!isExistingPending && (counts.pending >= CONTACT_REQUEST_MAX_PENDING
81
+ || counts.retained >= CONTACT_REQUEST_MAX_RETAINED)) {
82
+ return preparedWithExpirations(expiration, { code: 'capacity', contactRevision: '' }, logFile);
83
+ }
84
+ const requestId = `creq_${crypto.randomUUID().replace(/-/g, '')}`;
85
+ const expiresAt = new Date(nowMs + CONTACT_REQUEST_TTL_MS).toISOString();
86
+ const op = isExistingPending ? 'resubmit' : 'request';
87
+ const value = {
88
+ code: isExistingPending ? 'resubmitted' : 'submitted',
89
+ requestId,
90
+ submittedAt: nowIso,
91
+ expiresAt,
92
+ contactRevision: '',
93
+ };
94
+ const mutations = [
95
+ ...expiration.mutations,
96
+ {
97
+ type: 'set-request',
98
+ primaryId: applicantAid,
99
+ request: { id: requestId, submittedAt: nowIso, expiresAt },
100
+ },
101
+ ];
102
+ return {
103
+ mutation: { type: 'batch', mutations },
104
+ actor: applicantAid,
105
+ value,
106
+ beforeCommit: ({ contactRevision }) => appendOperationRecords(logFile, [
107
+ ...expiration.events.map(event => ({ ...event, contactRevision })),
108
+ operationRecord({
109
+ at: nowIso,
110
+ op,
111
+ primaryId: applicantAid,
112
+ requestId,
113
+ sourceChannelKey: input.sourceChannelKey,
114
+ ...(note ? { note } : {}),
115
+ contactRevision,
116
+ }),
117
+ ]),
118
+ };
119
+ });
120
+ return { ...result.value, contactRevision: result.contactRevision };
121
+ }
122
+ export async function reviewContactRequest(input) {
123
+ const nowMs = input.now ?? Date.now();
124
+ const nowIso = new Date(nowMs).toISOString();
125
+ const logFile = agentContactOperations(input.selfAid);
126
+ try {
127
+ const result = await mutateContactBookPrepared(input.selfAid, ({ contact, contactRevision }) => {
128
+ const owners = listStaticAgentOwners(input.selfAid, { fresh: true });
129
+ const admins = listStaticAgentAdmins(input.selfAid, { fresh: true });
130
+ const selectedManager = input.reviewerRole === undefined
131
+ && input.actorId === input.approverId
132
+ && (owners.includes(input.approverId) || admins.includes(input.approverId));
133
+ const managementReviewer = input.actorId === input.approverId
134
+ && ((input.reviewerRole === 'admin' && admins.includes(input.actorId))
135
+ || (input.reviewerRole === 'owner' && owners.includes(input.actorId)));
136
+ if (!selectedManager && !managementReviewer) {
137
+ return preparedNoop({ code: 'forbidden', contactRevision });
138
+ }
139
+ if (contactRevision !== input.expectedContactRevision) {
140
+ return preparedNoop({ code: 'revision-conflict', contactRevision });
141
+ }
142
+ const entry = contact.contacts[input.primaryId];
143
+ if (!entry || entry.status !== 'pending' || entry.pendingRequest?.id !== input.requestId) {
144
+ return preparedNoop({ code: 'request-stale', contactRevision });
145
+ }
146
+ if (Date.parse(entry.pendingRequest.expiresAt) <= nowMs) {
147
+ const mutation = {
148
+ type: 'expire-request',
149
+ primaryId: input.primaryId,
150
+ requestId: input.requestId,
151
+ now: nowIso,
152
+ };
153
+ return {
154
+ mutation,
155
+ actor: input.actorId,
156
+ value: { code: 'request-expired', contactRevision },
157
+ beforeCommit: ({ contactRevision: nextRevision }) => appendOperationRecords(logFile, [operationRecord({
158
+ at: nowIso,
159
+ op: 'expire',
160
+ primaryId: input.primaryId,
161
+ requestId: input.requestId,
162
+ contactRevision: nextRevision,
163
+ })]),
164
+ };
165
+ }
166
+ const code = input.decision === 'approve'
167
+ ? 'approved'
168
+ : input.decision === 'reject'
169
+ ? 'rejected'
170
+ : 'blocked';
171
+ return {
172
+ mutation: {
173
+ type: 'review-request',
174
+ primaryId: input.primaryId,
175
+ requestId: input.requestId,
176
+ decision: input.decision,
177
+ now: nowIso,
178
+ },
179
+ actor: input.actorId,
180
+ value: { code, contactRevision },
181
+ beforeCommit: ({ contactRevision: nextRevision }) => appendOperationRecords(logFile, [operationRecord({
182
+ at: nowIso,
183
+ op: input.decision === 'approve' ? 'approve' : input.decision === 'reject' ? 'reject' : 'block',
184
+ primaryId: input.primaryId,
185
+ requestId: input.requestId,
186
+ actorId: input.actorId,
187
+ contactRevision: nextRevision,
188
+ })]),
189
+ };
190
+ });
191
+ return { ...result.value, contactRevision: result.contactRevision };
192
+ }
193
+ catch (error) {
194
+ if (error instanceof ContactMutationError && error.code === 'REQUEST_EXPIRED') {
195
+ return { code: 'request-expired', contactRevision: input.expectedContactRevision };
196
+ }
197
+ throw error;
198
+ }
199
+ }
200
+ export async function expirePendingContactRequests(selfAid, now = Date.now()) {
201
+ const nowIso = new Date(now).toISOString();
202
+ const logFile = agentContactOperations(selfAid);
203
+ const result = await mutateContactBookPrepared(selfAid, ({ contact }) => {
204
+ const expiration = collectExpiredMutations(contact, now);
205
+ if (expiration.mutations.length === 0)
206
+ return preparedNoop(0);
207
+ return {
208
+ mutation: { type: 'batch', mutations: expiration.mutations },
209
+ actor: 'contact-request-expiry',
210
+ value: expiration.mutations.length,
211
+ beforeCommit: ({ contactRevision }) => appendOperationRecords(logFile, expiration.events.map(event => ({ ...event, contactRevision }))),
212
+ };
213
+ });
214
+ return result.value;
215
+ }
216
+ export async function listPendingContactRequests(selfAid, now = Date.now()) {
217
+ await expirePendingContactRequests(selfAid, now);
218
+ const result = await mutateContactBookPrepared(selfAid, ({ contact, contactRevision }) => preparedNoop(Object.entries(contact.contacts)
219
+ .filter(([, entry]) => entry.status === 'pending' && entry.pendingRequest)
220
+ .map(([primaryId, entry]) => ({
221
+ primaryId,
222
+ requestId: entry.pendingRequest.id,
223
+ submittedAt: entry.pendingRequest.submittedAt,
224
+ expiresAt: entry.pendingRequest.expiresAt,
225
+ contactRevision,
226
+ }))));
227
+ return result.value;
228
+ }
229
+ export function findContactRequestNote(selfAid, requestId) {
230
+ const records = readOperationLog(agentContactOperations(selfAid));
231
+ return [...records].reverse().find(record => (record.requestId === requestId
232
+ && (record.op === 'request' || record.op === 'resubmit')))?.note;
233
+ }
234
+ function normalizeNote(value) {
235
+ const note = String(value ?? '').trim();
236
+ if (note.length > CONTACT_REQUEST_NOTE_MAX_LENGTH)
237
+ return null;
238
+ return note;
239
+ }
240
+ function collectExpiredMutations(book, now) {
241
+ const mutations = [];
242
+ const events = [];
243
+ for (const [primaryId, entry] of Object.entries(book.contacts)) {
244
+ if (entry.status !== 'pending' || !entry.pendingRequest)
245
+ continue;
246
+ if (Date.parse(entry.pendingRequest.expiresAt) > now)
247
+ continue;
248
+ mutations.push({
249
+ type: 'expire-request',
250
+ primaryId,
251
+ requestId: entry.pendingRequest.id,
252
+ now: new Date(now).toISOString(),
253
+ });
254
+ events.push(operationRecord({
255
+ at: new Date(now).toISOString(),
256
+ op: 'expire',
257
+ primaryId,
258
+ requestId: entry.pendingRequest.id,
259
+ contactRevision: '',
260
+ }));
261
+ }
262
+ return { mutations, events };
263
+ }
264
+ function effectiveEntryAfterExpirations(entry, now) {
265
+ if (entry?.status === 'pending' && entry.pendingRequest && Date.parse(entry.pendingRequest.expiresAt) <= now) {
266
+ return { ...entry, status: 'declined', pendingRequest: undefined, declinedAt: entry.pendingRequest.expiresAt };
267
+ }
268
+ return entry;
269
+ }
270
+ function countRequestRecordsAfterExpirations(book, now) {
271
+ let pending = 0;
272
+ let declined = 0;
273
+ for (const entry of Object.values(book.contacts)) {
274
+ const effective = effectiveEntryAfterExpirations(entry, now);
275
+ if (effective?.status === 'pending')
276
+ pending += 1;
277
+ else if (effective?.status === 'declined')
278
+ declined += 1;
279
+ }
280
+ return { pending, retained: pending + declined };
281
+ }
282
+ function preparedWithExpirations(expiration, value, logFile) {
283
+ if (expiration.mutations.length === 0)
284
+ return preparedNoop(value);
285
+ return {
286
+ mutation: { type: 'batch', mutations: expiration.mutations },
287
+ actor: 'contact-request-expiry',
288
+ value,
289
+ beforeCommit: ({ contactRevision }) => appendOperationRecords(logFile, expiration.events.map(event => ({ ...event, contactRevision }))),
290
+ };
291
+ }
292
+ function preparedNoop(value) {
293
+ return { value };
294
+ }
295
+ function readOperationLog(file) {
296
+ let raw;
297
+ try {
298
+ raw = fs.readFileSync(file, 'utf8');
299
+ }
300
+ catch (error) {
301
+ if (error?.code === 'ENOENT')
302
+ return [];
303
+ throw new ContactMutationError('CONTACT_OPERATION_LOG_INVALID', `Cannot read contact operation log: ${error instanceof Error ? error.message : String(error)}`);
304
+ }
305
+ const records = [];
306
+ for (const [index, line] of raw.split('\n').entries()) {
307
+ if (!line.trim())
308
+ continue;
309
+ try {
310
+ const record = JSON.parse(line);
311
+ if (!record || typeof record !== 'object' || typeof record.at !== 'string'
312
+ || !Number.isFinite(Date.parse(record.at)) || typeof record.op !== 'string'
313
+ || typeof record.primaryId !== 'string')
314
+ throw new Error('invalid record');
315
+ records.push(record);
316
+ }
317
+ catch (error) {
318
+ throw new ContactMutationError('CONTACT_OPERATION_LOG_INVALID', `Invalid contact operation log record at line ${index + 1}: ${error instanceof Error ? error.message : String(error)}`);
319
+ }
320
+ }
321
+ return records;
322
+ }
323
+ function appendOperationRecords(file, records) {
324
+ 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');
328
+ }
329
+ function operationRecord(input) {
330
+ return { id: `evt_${crypto.randomUUID().replace(/-/g, '')}`, ...input };
331
+ }
@@ -133,7 +133,9 @@ export function roleToSessionIdentity(role) {
133
133
  export function checkRoleAccess(role, selfAid) {
134
134
  if (!role)
135
135
  return false;
136
- if (isManagementRole(role))
136
+ // Owner access is an invariant. Admin access is role policy, just like the
137
+ // user roles, so a local admin definition may explicitly disable it.
138
+ if (role === 'owner')
137
139
  return true;
138
140
  const definition = getRoleDefinition(role, selfAid);
139
141
  return !!definition && (definition.allowAccess ?? true);
@@ -14,6 +14,7 @@ import fs from 'fs';
14
14
  import path from 'path';
15
15
  import Ajv from 'ajv';
16
16
  import { kitsSchemasDir } from '../paths.js';
17
+ import { createErrorCapturingReader, fileCache } from '../core/daemon-file-cache.js';
17
18
  let _ajv = null;
18
19
  function ajv() {
19
20
  if (!_ajv) {
@@ -21,13 +22,23 @@ function ajv() {
21
22
  }
22
23
  return _ajv;
23
24
  }
24
- let _meta = null;
25
+ /**
26
+ * schema 随包发布,运行期不查盘 → fileCache 的 on-reload 策略,归入 'kits' 组
27
+ * (与 manifest/fragment 同组,一次 invalidateGroup('kits') 全刷)。
28
+ * 刷新时机只有三个:进程重启、`ec agent reload`、resync——后两者在调 registry.reload
29
+ * 之前先 invalidateKitCache()。带外改 schema 文件而不做上述任一动作,进程不会看见。
30
+ */
31
+ const SCHEMA_CACHE_OPTS = { policy: 'on-reload', group: 'kits' };
25
32
  export function loadMeta() {
26
- if (_meta)
27
- return _meta;
28
33
  const p = path.join(kitsSchemasDir(), '_meta.json');
29
- _meta = JSON.parse(fs.readFileSync(p, 'utf-8'));
30
- return _meta;
34
+ const reader = createErrorCapturingReader();
35
+ return fileCache.get(p, (raw) => {
36
+ // errno 文案自带路径,不再重复拼 p;原始异常挂 cause,保住 code/path/errno
37
+ if (raw === null) {
38
+ throw new Error(`[schema] cannot read meta: ${reader.lastError()}`, { cause: reader.lastErrorCause() });
39
+ }
40
+ return JSON.parse(raw);
41
+ }, { ...SCHEMA_CACHE_OPTS, read: reader.read });
31
42
  }
32
43
  /** 某 schema 的当前版本号(来自 _meta.json)。 */
33
44
  export function currentVersion(name) {
@@ -84,7 +95,6 @@ export function readRawSchema(name, version) {
84
95
  }
85
96
  return JSON.parse(fs.readFileSync(file, 'utf-8'));
86
97
  }
87
- const _cache = new Map();
88
98
  function schemaFilePath(name, version) {
89
99
  return path.join(kitsSchemasDir(), `${name}.schema.${version}.json`);
90
100
  }
@@ -117,22 +127,35 @@ function inferMerge(spec) {
117
127
  /** 加载指定版本的 schema entry(带 ajv 编译 + 字段表)。 */
118
128
  export function loadSchema(name, version) {
119
129
  const ver = version ?? currentVersion(name);
120
- const key = `${name}@${ver}`;
121
- const cached = _cache.get(key);
122
- if (cached)
123
- return cached;
124
130
  const file = schemaFilePath(name, ver);
125
- const raw = JSON.parse(fs.readFileSync(file, 'utf-8'));
126
- const entry = {
127
- logicalName: name,
128
- version: ver,
129
- scope: raw['x-scope'] || name,
130
- raw,
131
- validate: ajv().compile(raw),
132
- fields: extractFields(raw),
133
- };
134
- _cache.set(key, entry);
135
- return entry;
131
+ const reader = createErrorCapturingReader();
132
+ return fileCache.get(file, (rawText) => {
133
+ // errno 文案自带路径,不再重复拼 file;原始异常挂 cause,保住 code/path/errno
134
+ if (rawText === null) {
135
+ throw new Error(`[schema] cannot read schema file: ${reader.lastError()}`, { cause: reader.lastErrorCause() });
136
+ }
137
+ const raw = JSON.parse(rawText);
138
+ return {
139
+ logicalName: name,
140
+ version: ver,
141
+ scope: raw['x-scope'] || name,
142
+ raw,
143
+ validate: compileSchema(raw),
144
+ fields: extractFields(raw),
145
+ };
146
+ }, { ...SCHEMA_CACHE_OPTS, read: reader.read });
147
+ }
148
+ /**
149
+ * 每个 schema 文件都带 `$id`,而 ajv 对同一实例重复注册同名 `$id` 会抛
150
+ * "schema with key or id ... already exists"。缓存失效后会重新 JSON.parse 出
151
+ * 一个新对象(ajv 的对象级缓存认不出),因此编译前先按 `$id` 撤销旧注册,
152
+ * 让 reload → 重新编译这条路径可重复执行。
153
+ */
154
+ function compileSchema(raw) {
155
+ const id = raw?.$id;
156
+ if (typeof id === 'string')
157
+ ajv().removeSchema(id);
158
+ return ajv().compile(raw);
136
159
  }
137
160
  /** extra_backup 不得指向 .env(构建期校验)——遍历声明里的 pattern。 */
138
161
  export function assertExtraBackupNotEnv(extraBackup) {
@@ -146,9 +169,11 @@ export function assertExtraBackupNotEnv(extraBackup) {
146
169
  }
147
170
  }
148
171
  }
149
- /** 测试用:清空缓存。 */
172
+ /**
173
+ * 测试用:清空缓存。schema 与 _meta 都存在 fileCache 的 'kits' 组,
174
+ * 连同 ajv 实例一起重建(丢弃已注册的 $id,避免跨用例串味)。
175
+ */
150
176
  export function _resetSchemaCache() {
151
- _cache.clear();
152
- _meta = null;
177
+ fileCache.invalidateGroup('kits');
153
178
  _ajv = null;
154
179
  }
@@ -25,10 +25,11 @@ import { CONFIG_SCHEMA_VERSION } from './types.js';
25
25
  import { ConfigTarget, read as cfgRead, write as cfgWrite } from './config/config-manager.js';
26
26
  import { expandVars, buildEnvResolver } from './config/merge.js';
27
27
  import { logger } from './utils/logger.js';
28
+ import { parseStableSemver } from './utils/stable-semver.js';
28
29
  /** 读 {root}/daemon.json。文件不存在返回 {},不报错。 */
29
30
  export function loadDaemonConfig() {
30
31
  const raw = atomicReadJson(resolvePaths().daemonConfig);
31
- return raw ?? {};
32
+ return validateDaemonConfig(raw ?? {});
32
33
  }
33
34
  let eckSnapshotsConfigCache;
34
35
  /** Parse the process-level snapshot gate once for the current daemon lifecycle. */
@@ -49,7 +50,18 @@ export function isEckSnapshotsEnabled() {
49
50
  }
50
51
  /** 原子写入 {root}/daemon.json。调用方负责传完整对象(含要保留的字段)。 */
51
52
  export function saveDaemonConfig(value) {
52
- atomicWriteJson(resolvePaths().daemonConfig, value);
53
+ atomicWriteJson(resolvePaths().daemonConfig, validateDaemonConfig(value));
54
+ }
55
+ function validateDaemonConfig(value) {
56
+ const minEvolVersion = value.aun?.minEvolVersion;
57
+ if (minEvolVersion !== undefined && !parseStableSemver(minEvolVersion)) {
58
+ throw new Error('daemon.json.aun.minEvolVersion must use stable X.Y.Z format');
59
+ }
60
+ const menuTokenRequired = value.aun?.menuTokenRequired;
61
+ if (menuTokenRequired !== undefined && typeof menuTokenRequired !== 'boolean') {
62
+ throw new Error('daemon.json.aun.menuTokenRequired must be a boolean');
63
+ }
64
+ return value;
53
65
  }
54
66
  const SUPPORTED_CHANNEL_TYPES = new Set([
55
67
  'aun', 'feishu', 'wechat', 'dingtalk', 'qqbot', 'wecom',
@@ -2,9 +2,11 @@ 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
6
  export class AgentDelegationRegistry {
6
7
  grantsByHash = new Map();
7
8
  activeHashBySession = new Map();
9
+ approvedCommands = new Map();
8
10
  issue(input) {
9
11
  this.revokeSession(input.sessionId);
10
12
  const token = crypto.randomBytes(32).toString('base64url');
@@ -13,7 +15,7 @@ export class AgentDelegationRegistry {
13
15
  this.activeHashBySession.set(input.sessionId, tokenHash);
14
16
  return token;
15
17
  }
16
- validate(token, sessionId) {
18
+ validate(token, sessionId, commandHash) {
17
19
  if (!token) {
18
20
  return {
19
21
  ok: false,
@@ -23,16 +25,49 @@ export class AgentDelegationRegistry {
23
25
  }
24
26
  const tokenHash = hashDelegationToken(token);
25
27
  const grant = this.grantsByHash.get(tokenHash);
26
- if (!grant
27
- || grant.sessionId !== sessionId
28
- || this.activeHashBySession.get(sessionId) !== tokenHash) {
29
- return {
30
- ok: false,
31
- code: 'INVALID_DELEGATION',
32
- reason: 'The task delegation token is invalid, revoked, or belongs to another session',
33
- };
28
+ if (grant
29
+ && grant.sessionId === sessionId
30
+ && this.activeHashBySession.get(sessionId) === tokenHash) {
31
+ return { ok: true, grant };
34
32
  }
35
- return { ok: true, grant };
33
+ const approved = commandHash
34
+ ? this.consumeApprovedCommand(tokenHash, sessionId, commandHash)
35
+ : undefined;
36
+ if (approved)
37
+ return { ok: true, grant: approved };
38
+ return {
39
+ ok: false,
40
+ code: 'INVALID_DELEGATION',
41
+ reason: 'The task delegation token is invalid, revoked, or belongs to another session',
42
+ };
43
+ }
44
+ armApprovedCommand(input) {
45
+ const activeTokenHash = this.activeHashBySession.get(input.sessionId);
46
+ if (!activeTokenHash)
47
+ return false;
48
+ const grant = activeTokenHash ? this.grantsByHash.get(activeTokenHash) : undefined;
49
+ if (!grant || grant.taskId !== input.taskId)
50
+ return false;
51
+ if (!input.carrierToken || !isDelegationCommandHash(input.commandHash))
52
+ return false;
53
+ this.deleteExpiredApprovedCommands();
54
+ const carrierTokenHash = hashDelegationToken(input.carrierToken);
55
+ const key = approvedCommandKey(carrierTokenHash, input.sessionId, input.commandHash);
56
+ const existing = this.approvedCommands.get(key);
57
+ const ttlMs = Math.max(1, Math.min(input.ttlMs ?? AGENT_DELEGATION_COMMAND_TTL_MS, AGENT_DELEGATION_COMMAND_TTL_MS));
58
+ this.approvedCommands.set(key, {
59
+ sessionId: input.sessionId,
60
+ taskId: input.taskId,
61
+ activeTokenHash,
62
+ commandHash: input.commandHash,
63
+ expiresAt: Date.now() + ttlMs,
64
+ remainingUses: existing
65
+ && existing.activeTokenHash === activeTokenHash
66
+ && existing.expiresAt > Date.now()
67
+ ? existing.remainingUses + 1
68
+ : 1,
69
+ });
70
+ return true;
36
71
  }
37
72
  revokeTask(sessionId, taskId) {
38
73
  const tokenHash = this.activeHashBySession.get(sessionId);
@@ -43,19 +78,59 @@ export class AgentDelegationRegistry {
43
78
  return;
44
79
  this.grantsByHash.delete(tokenHash);
45
80
  this.activeHashBySession.delete(sessionId);
81
+ this.deleteApprovedCommandsForSession(sessionId);
46
82
  }
47
83
  revokeSession(sessionId) {
48
84
  const tokenHash = this.activeHashBySession.get(sessionId);
49
85
  if (tokenHash)
50
86
  this.grantsByHash.delete(tokenHash);
51
87
  this.activeHashBySession.delete(sessionId);
88
+ this.deleteApprovedCommandsForSession(sessionId);
89
+ }
90
+ consumeApprovedCommand(carrierTokenHash, sessionId, commandHash) {
91
+ if (!isDelegationCommandHash(commandHash))
92
+ return undefined;
93
+ const key = approvedCommandKey(carrierTokenHash, sessionId, commandHash);
94
+ const approved = this.approvedCommands.get(key);
95
+ if (!approved)
96
+ return undefined;
97
+ if (approved.expiresAt <= Date.now()) {
98
+ this.approvedCommands.delete(key);
99
+ return undefined;
100
+ }
101
+ const activeTokenHash = this.activeHashBySession.get(sessionId);
102
+ const grant = activeTokenHash ? this.grantsByHash.get(activeTokenHash) : undefined;
103
+ if (!grant
104
+ || activeTokenHash !== approved.activeTokenHash
105
+ || grant.taskId !== approved.taskId) {
106
+ this.approvedCommands.delete(key);
107
+ return undefined;
108
+ }
109
+ if (approved.remainingUses <= 1)
110
+ this.approvedCommands.delete(key);
111
+ else
112
+ approved.remainingUses--;
113
+ return grant;
114
+ }
115
+ deleteExpiredApprovedCommands() {
116
+ const now = Date.now();
117
+ for (const [key, approved] of this.approvedCommands) {
118
+ if (approved.expiresAt <= now)
119
+ this.approvedCommands.delete(key);
120
+ }
121
+ }
122
+ deleteApprovedCommandsForSession(sessionId) {
123
+ for (const [key, approved] of this.approvedCommands) {
124
+ if (approved.sessionId === sessionId)
125
+ this.approvedCommands.delete(key);
126
+ }
52
127
  }
53
128
  }
54
129
  export function authorizeDelegatedAunMsgSend(registry, input) {
55
130
  if (!input.sessionId) {
56
131
  return { ok: false, code: 'DELEGATION_REQUIRED', reason: 'Origin session is required' };
57
132
  }
58
- const validation = registry.validate(input.delegationToken, input.sessionId);
133
+ const validation = registry.validate(input.delegationToken, input.sessionId, input.delegationCommandHash);
59
134
  if (!validation.ok)
60
135
  return validation;
61
136
  const grant = validation.grant;
@@ -109,3 +184,22 @@ export function authorizeDelegatedAunMsgSend(registry, input) {
109
184
  function hashDelegationToken(token) {
110
185
  return crypto.createHash('sha256').update(token).digest('hex');
111
186
  }
187
+ function approvedCommandKey(carrierTokenHash, sessionId, commandHash) {
188
+ return JSON.stringify([carrierTokenHash, sessionId, commandHash]);
189
+ }
190
+ function isDelegationCommandHash(value) {
191
+ return /^[a-f0-9]{64}$/.test(value);
192
+ }
193
+ export function hashDelegatedCommandArgv(argv) {
194
+ if (argv.length === 0 || argv.some(value => typeof value !== 'string' || value.includes('\0')))
195
+ return undefined;
196
+ return crypto.createHash('sha256')
197
+ .update('evolcore-agent-delegation-command-v1\0')
198
+ .update(JSON.stringify(argv))
199
+ .digest('hex');
200
+ }
201
+ export function hashCurrentDelegatedCommand(processArgv = process.argv) {
202
+ if (processArgv.length < 3)
203
+ return undefined;
204
+ return hashDelegatedCommandArgv(['ec', ...processArgv.slice(2)]);
205
+ }
@@ -21,6 +21,9 @@ function buildAuditRecord(event) {
21
21
  operation: event.operation,
22
22
  scope: event.scope,
23
23
  dangerous: event.dangerous,
24
+ name: event.name,
25
+ action: event.action,
26
+ args: event.args?.argv ? { argv: [...event.args.argv] } : undefined,
24
27
  actorId: redactIdentifier(event.actorId),
25
28
  selfAid: redactIdentifier(event.selfAid),
26
29
  peerKey: redactIdentifier(event.peerKey),
@@ -61,6 +64,9 @@ function logAuditEvent(record) {
61
64
  const message = [
62
65
  `[CommandAudit:${marker}]`,
63
66
  `operation=${record.operation}`,
67
+ record.name ? `name=${record.name}` : null,
68
+ record.action ? `action=${record.action}` : null,
69
+ record.args?.argv ? `args.argv=${JSON.stringify(record.args.argv)}` : null,
64
70
  `role=${record.role}`,
65
71
  `actor=${record.actorId || 'unknown'}`,
66
72
  record.taskId ? `task=${record.taskId}` : null,