evolcore 0.0.2 → 0.0.4

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 (206) hide show
  1. package/CHANGELOG.md +93 -778
  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 +198 -48
  6. package/dist/agents/codex-app-server-client.js +68 -0
  7. package/dist/agents/codex-runner.js +203 -8
  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/aun/outbox.js +8 -0
  14. package/dist/channels/aun.js +241 -116
  15. package/dist/channels/daemon.js +18 -4
  16. package/dist/channels/dingtalk.js +1005 -260
  17. package/dist/channels/feishu.js +184 -56
  18. package/dist/channels/qqbot.js +23 -1
  19. package/dist/channels/wechat.js +303 -155
  20. package/dist/channels/wecom.js +1132 -164
  21. package/dist/cli/agent-command.js +2 -1
  22. package/dist/cli/aun-commands.js +98 -43
  23. package/dist/cli/bench.js +2 -2
  24. package/dist/cli/contact.js +71 -0
  25. package/dist/cli/ctl-command.js +2 -2
  26. package/dist/cli/daemon-commands.js +77 -214
  27. package/dist/cli/handoff-command.js +4 -3
  28. package/dist/cli/help.js +9 -5
  29. package/dist/cli/index.js +132 -116
  30. package/dist/cli/init-channel.js +242 -129
  31. package/dist/cli/init.js +70 -31
  32. package/dist/cli/model.js +2 -1
  33. package/dist/cli/net-check.js +2 -2
  34. package/dist/cli/queue-command.js +30 -6
  35. package/dist/cli/raw-key-input.js +25 -0
  36. package/dist/cli/response.js +5 -6
  37. package/dist/cli/restart-monitor.js +25 -1
  38. package/dist/cli/stats.js +6 -4
  39. package/dist/cli/trigger-command.js +146 -25
  40. package/dist/cli/version.js +6 -1
  41. package/dist/config/access-policy-domain.js +38 -0
  42. package/dist/config/access-policy.js +134 -0
  43. package/dist/config/builtin-role-templates.js +42 -17
  44. package/dist/config/builtin-roles.js +31 -6
  45. package/dist/config/config-field-policy.js +17 -5
  46. package/dist/config/config-manager.js +409 -29
  47. package/dist/config/config-operation-service.js +35 -38
  48. package/dist/config/contact-alias.js +68 -0
  49. package/dist/config/contact-bind-code.js +274 -0
  50. package/dist/config/contact-book-store.js +622 -0
  51. package/dist/config/contact-book-v2-startup.js +35 -0
  52. package/dist/config/contact-book.js +185 -300
  53. package/dist/config/contact-operation-service.js +116 -0
  54. package/dist/config/contact-request-service.js +331 -0
  55. package/dist/config/peer-role-resolver.js +136 -55
  56. package/dist/config/role-ranks.js +18 -0
  57. package/dist/config/role-service.js +16 -19
  58. package/dist/config/role-store.js +16 -5
  59. package/dist/config/roles.js +10 -1
  60. package/dist/config/schema-registry.js +49 -24
  61. package/dist/config-store.js +14 -4
  62. package/dist/core/auth/agent-delegation.js +105 -11
  63. package/dist/core/auth/authorization-audit.js +16 -1
  64. package/dist/core/auth/operation-authorizer.js +10 -0
  65. package/dist/core/auth/operation-catalog.js +105 -1
  66. package/dist/core/auth/trigger-authorization.js +15 -0
  67. package/dist/core/bootstrap-service.js +2 -9
  68. package/dist/core/channel-loader.js +6 -2
  69. package/dist/core/command/command-handler.js +113 -21
  70. package/dist/core/command/connect-menu.js +599 -0
  71. package/dist/core/command/evol-menu-version-gate.js +38 -0
  72. package/dist/core/command/group-menu.js +421 -0
  73. package/dist/core/command/menu-handler.js +382 -100
  74. package/dist/core/command/menu-protocol.js +8 -2
  75. package/dist/core/command/menu-token-store.js +102 -0
  76. package/dist/core/command/role-menu.js +128 -29
  77. package/dist/core/command/slash-gate.js +1 -1
  78. package/dist/core/command/slash-handler.js +186 -49
  79. package/dist/core/daemon-file-cache.js +40 -6
  80. package/dist/core/event-catalog.js +99 -0
  81. package/dist/core/evolagent-registry.js +4 -40
  82. package/dist/core/evolagent.js +20 -9
  83. package/dist/core/handoff/runtime.js +162 -37
  84. package/dist/core/handoff/store.js +46 -2
  85. package/dist/core/handoff/types.js +1 -0
  86. package/dist/core/inference/text-inference.js +16 -74
  87. package/dist/core/message/file-markers.js +7 -0
  88. package/dist/core/message/im-renderer.js +20 -14
  89. package/dist/core/message/inbound-admission.js +124 -0
  90. package/dist/core/message/message-bridge.js +670 -57
  91. package/dist/core/message/message-log.js +1 -0
  92. package/dist/core/message/message-queue.js +195 -10
  93. package/dist/core/message/message-utils.js +8 -2
  94. package/dist/core/message/peer-mode.js +7 -8
  95. package/dist/core/message/response-engine.js +642 -96
  96. package/dist/core/message/send-receipt.js +24 -0
  97. package/dist/core/message/stream-debouncer.js +11 -2
  98. package/dist/core/permission/approval-gateway.js +17 -10
  99. package/dist/core/permission/ec-command-parser.js +101 -46
  100. package/dist/core/permission/tool-policy.js +115 -38
  101. package/dist/core/protected-paths.js +38 -11
  102. package/dist/core/session/session-fs-store.js +44 -3
  103. package/dist/core/session/session-manager.js +86 -8
  104. package/dist/core/session/session-mapper.js +2 -0
  105. package/dist/core/session/session-renew.js +125 -69
  106. package/dist/core/session/session-turn-coordinator.js +5 -1
  107. package/dist/eck/kit-renderer.js +12 -1
  108. package/dist/eck/message-renderer.js +79 -1
  109. package/dist/index.js +285 -94
  110. package/dist/ipc.js +114 -8
  111. package/dist/paths.js +3 -0
  112. package/dist/response-system/config-resolver.js +29 -0
  113. package/dist/response-system/coordinator.js +8 -32
  114. package/dist/response-system/engines/v1/proactive-flow.js +1 -1
  115. package/dist/response-system/index.js +1 -0
  116. package/dist/response-system/modes/single-session/index.js +7 -4
  117. package/dist/stats/billing.js +20 -8
  118. package/dist/trigger/manager.js +3 -0
  119. package/dist/trigger/parser.js +126 -11
  120. package/dist/trigger/patch.js +12 -2
  121. package/dist/trigger/scheduler.js +444 -40
  122. package/dist/trigger/validation.js +13 -0
  123. package/dist/utils/aid-bind.js +43 -29
  124. package/dist/utils/error-dict.json +7 -0
  125. package/dist/utils/evolcore-version.js +21 -0
  126. package/dist/utils/instance-registry.js +14 -7
  127. package/dist/utils/log-writer.js +46 -0
  128. package/dist/utils/logger.js +2 -2
  129. package/dist/utils/media-cache.js +4 -1
  130. package/dist/utils/model-prices.jsonl +6 -3
  131. package/dist/utils/restart-safety.js +31 -0
  132. package/dist/utils/stable-semver.js +21 -0
  133. package/dist/utils/stats.js +33 -9
  134. package/dist/utils/system-memory.js +62 -0
  135. package/kits/docs/INDEX.md +2 -1
  136. package/kits/docs/channels/aun.md +4 -13
  137. package/kits/docs/evolcore/INDEX.md +5 -3
  138. package/kits/docs/evolcore/agent.md +9 -1
  139. package/kits/docs/evolcore/aid.md +5 -2
  140. package/kits/docs/evolcore/config.md +47 -2
  141. package/kits/docs/evolcore/contact.md +61 -0
  142. package/kits/docs/evolcore/fs.md +9 -0
  143. package/kits/docs/evolcore/group-rules.md +46 -4
  144. package/kits/docs/evolcore/group.md +15 -6
  145. package/kits/docs/evolcore/model.md +4 -1
  146. package/kits/docs/evolcore/msg.md +12 -6
  147. package/kits/docs/evolcore/response.md +16 -21
  148. package/kits/docs/evolcore/rpc.md +2 -0
  149. package/kits/docs/evolcore/stats.md +15 -2
  150. package/kits/docs/evolcore/storage.md +1 -0
  151. package/kits/docs/evolcore/trigger.md +42 -10
  152. package/kits/docs/path-registry.md +36 -17
  153. package/kits/eck_manifest.json +12 -0
  154. package/kits/eck_message_manifest.json +12 -1
  155. package/kits/migrations/migrate-contact-book-v2.mjs +754 -0
  156. package/kits/rules/01-overview.md +17 -7
  157. package/kits/rules/02-navigation.md +38 -18
  158. package/kits/rules/03-identity.md +28 -24
  159. package/kits/rules/04-relation.md +44 -28
  160. package/kits/rules/05-venue.md +31 -15
  161. package/kits/rules/06-channel.md +11 -7
  162. package/kits/schemas/_meta.json +21 -7
  163. package/kits/schemas/agent-config.schema.6.json +322 -0
  164. package/kits/schemas/agent-config.schema.7.json +303 -0
  165. package/kits/schemas/agent-config.schema.8.json +304 -0
  166. package/kits/schemas/agent-config.schema.9.json +374 -0
  167. package/kits/schemas/contact-book.schema.2.json +43 -0
  168. package/kits/schemas/contact-book.schema.3.json +67 -0
  169. package/kits/schemas/daemon.schema.1.json +3 -2
  170. package/kits/schemas/daemon.schema.2.json +101 -0
  171. package/kits/schemas/daemon.schema.3.json +123 -0
  172. package/kits/schemas/defaults.schema.2.json +85 -0
  173. package/kits/schemas/defaults.schema.3.json +73 -0
  174. package/kits/schemas/relation-config.schema.5.json +47 -0
  175. package/kits/schemas/relation-config.schema.6.json +46 -0
  176. package/kits/schemas/relation-config.schema.7.json +59 -0
  177. package/kits/schemas/role-config.schema.1.json +1 -0
  178. package/kits/schemas/role-registry.schema.1.json +2 -2
  179. package/kits/schemas/single-session.schema.2.json +57 -0
  180. package/kits/templates/message-fragments/handoff-context-to-target.md +9 -0
  181. package/kits/templates/message-fragments/handoff-request-to-target.md +14 -8
  182. package/kits/templates/message-fragments/handoff-response-to-origin.md +5 -6
  183. package/kits/templates/roles/admin.json +47 -0
  184. package/kits/templates/roles/member.json +5 -0
  185. package/kits/templates/roles/owner.json +1 -0
  186. package/kits/templates/roles/visitor.json +5 -0
  187. package/kits/templates/system-fragments/channel.md +12 -2
  188. package/kits/templates/system-fragments/commands.md +3 -1
  189. package/kits/templates/system-fragments/identity.md +3 -1
  190. package/kits/templates/system-fragments/relation.md +1 -1
  191. package/kits/templates/system-fragments/session.md +6 -2
  192. package/package.json +7 -5
  193. package/MIGRATION-0.5.0.md +0 -378
  194. package/ROLE_ACCESS_CONTROL.md +0 -174
  195. package/assets/brand/evolcore/README.md +0 -19
  196. package/assets/brand/evolcore/evolcore-app-icon.png +0 -0
  197. package/assets/brand/evolcore/evolcore-app-icon.svg +0 -13
  198. package/assets/brand/evolcore/evolcore-brand-board.png +0 -0
  199. package/assets/brand/evolcore/evolcore-brand-board.svg +0 -126
  200. package/assets/brand/evolcore/evolcore-logo-kit.zip +0 -0
  201. package/assets/brand/evolcore/evolcore-logo-reverse.png +0 -0
  202. package/assets/brand/evolcore/evolcore-logo-reverse.svg +0 -14
  203. package/assets/brand/evolcore/evolcore-logo.png +0 -0
  204. package/assets/brand/evolcore/evolcore-logo.svg +0 -14
  205. package/assets/brand/evolcore/evolcore-mark.png +0 -0
  206. package/assets/brand/evolcore/evolcore-mark.svg +0 -10
@@ -0,0 +1,116 @@
1
+ import fs from 'fs';
2
+ import { isValidAid } from '../aun/aid/validation.js';
3
+ import { isExplicitGroupId } from '../aun/group-identity.js';
4
+ import { agentConfig } from '../paths.js';
5
+ import { getContactSnapshot, resolveContactView } from './contact-book.js';
6
+ import { ContactMutationError } from './contact-book-store.js';
7
+ import { mutateContactWithOperation } from './contact-request-service.js';
8
+ export function resolveContactCommand(argv) {
9
+ const args = argv[0] === 'contact' ? argv.slice(1) : [...argv];
10
+ const selfAid = String(argValue(args, '--self') || '').trim();
11
+ if (!isValidAid(selfAid))
12
+ return invalid('INVALID_SELF', '--self must be a valid Agent AID');
13
+ const positional = stripOptions(args);
14
+ const [command, subcommand, ...extra] = positional;
15
+ if (extra.length > 0)
16
+ return invalid('INVALID_ARGUMENT', `Unexpected contact arguments: ${extra.join(' ')}`);
17
+ if (command === 'list') {
18
+ if (subcommand !== undefined || !args.includes('--blocked')) {
19
+ return invalid('INVALID_ARGUMENT', 'Only ec contact list --blocked is currently supported');
20
+ }
21
+ return {
22
+ ok: true,
23
+ command: { kind: 'list-blocked', operationId: 'contact.read', selfAid, canonicalArgv: canonicalArgv(argv) },
24
+ };
25
+ }
26
+ if (command === 'block' || command === 'unblock') {
27
+ if (!isValidAid(subcommand || '') || isExplicitGroupId(subcommand || '')) {
28
+ return invalid('INVALID_AID', 'primaryId must be a valid individual AID');
29
+ }
30
+ return {
31
+ ok: true,
32
+ command: {
33
+ kind: 'set-blocked',
34
+ operationId: 'contact.block',
35
+ selfAid,
36
+ primaryId: subcommand,
37
+ blocked: command === 'block',
38
+ canonicalArgv: canonicalArgv(argv),
39
+ },
40
+ };
41
+ }
42
+ return invalid('UNKNOWN_COMMAND', `Unknown contact command: ${positional.join(' ')}`);
43
+ }
44
+ export async function executeResolvedContactCommand(command, actor = 'local-cli') {
45
+ if (!fs.existsSync(agentConfig(command.selfAid))) {
46
+ return { ok: false, code: 'AGENT_NOT_FOUND', error: `Agent does not exist: ${command.selfAid}` };
47
+ }
48
+ try {
49
+ if (command.kind === 'list-blocked') {
50
+ const snapshot = getContactSnapshot(command.selfAid);
51
+ return {
52
+ ok: true,
53
+ operation: 'list-blocked',
54
+ selfAid: command.selfAid,
55
+ contactRevision: snapshot.contactRevision,
56
+ items: [...snapshot.blockedIndex].sort().map(id => resolveContactView(command.selfAid, id)),
57
+ };
58
+ }
59
+ if (command.kind === 'set-blocked') {
60
+ if (!getContactSnapshot(command.selfAid).contacts.has(command.primaryId)) {
61
+ return { ok: false, code: 'NOT_FOUND', error: `Contact was not found: ${command.primaryId}` };
62
+ }
63
+ const result = await mutateContactWithOperation({
64
+ selfAid: command.selfAid,
65
+ actorId: actor,
66
+ operation: command.blocked ? 'block' : 'unblock',
67
+ primaryId: command.primaryId,
68
+ mutation: {
69
+ type: 'set-status',
70
+ primaryId: command.primaryId,
71
+ status: command.blocked ? 'blocked' : 'active',
72
+ },
73
+ });
74
+ return {
75
+ ok: true,
76
+ operation: 'set-blocked',
77
+ selfAid: command.selfAid,
78
+ primaryId: command.primaryId,
79
+ blocked: command.blocked,
80
+ changed: result.changed,
81
+ contactRevision: result.contactRevision,
82
+ };
83
+ }
84
+ return { ok: false, code: 'CONTACT_OPERATION_FAILED', error: 'Unsupported contact operation' };
85
+ }
86
+ catch (error) {
87
+ if (error instanceof ContactMutationError) {
88
+ return { ok: false, code: error.code, error: error.message, ...(error.data ? { data: error.data } : {}) };
89
+ }
90
+ return { ok: false, code: 'CONTACT_OPERATION_FAILED', error: error instanceof Error ? error.message : String(error) };
91
+ }
92
+ }
93
+ function argValue(args, flag) {
94
+ const index = args.indexOf(flag);
95
+ return index >= 0 ? args[index + 1] : undefined;
96
+ }
97
+ function stripOptions(args) {
98
+ const result = [];
99
+ for (let index = 0; index < args.length; index += 1) {
100
+ const arg = args[index];
101
+ if (arg === '--self' || arg === '--format') {
102
+ index += 1;
103
+ continue;
104
+ }
105
+ if (arg === '--blocked')
106
+ continue;
107
+ result.push(arg);
108
+ }
109
+ return result;
110
+ }
111
+ function canonicalArgv(argv) {
112
+ return argv[0] === 'contact' ? [...argv] : ['contact', ...argv];
113
+ }
114
+ function invalid(code, reason) {
115
+ return { ok: false, code, reason };
116
+ }
@@ -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
+ }