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.
- package/CHANGELOG.md +93 -778
- package/README.md +45 -10
- package/bin/ec-safe-output.js +89 -24
- package/dist/agents/baseagent.js +46 -0
- package/dist/agents/claude-runner.js +198 -48
- package/dist/agents/codex-app-server-client.js +68 -0
- package/dist/agents/codex-runner.js +203 -8
- package/dist/agents/runner-types.js +2 -2
- package/dist/aun/aid/agentmd.js +79 -0
- package/dist/aun/aid/encryption-seed-policy.js +29 -0
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/aid/store.js +2 -5
- package/dist/aun/outbox.js +8 -0
- package/dist/channels/aun.js +241 -116
- package/dist/channels/daemon.js +18 -4
- package/dist/channels/dingtalk.js +1005 -260
- package/dist/channels/feishu.js +184 -56
- package/dist/channels/qqbot.js +23 -1
- package/dist/channels/wechat.js +303 -155
- package/dist/channels/wecom.js +1132 -164
- package/dist/cli/agent-command.js +2 -1
- package/dist/cli/aun-commands.js +98 -43
- package/dist/cli/bench.js +2 -2
- package/dist/cli/contact.js +71 -0
- package/dist/cli/ctl-command.js +2 -2
- package/dist/cli/daemon-commands.js +77 -214
- package/dist/cli/handoff-command.js +4 -3
- package/dist/cli/help.js +9 -5
- package/dist/cli/index.js +132 -116
- package/dist/cli/init-channel.js +242 -129
- package/dist/cli/init.js +70 -31
- package/dist/cli/model.js +2 -1
- package/dist/cli/net-check.js +2 -2
- package/dist/cli/queue-command.js +30 -6
- package/dist/cli/raw-key-input.js +25 -0
- package/dist/cli/response.js +5 -6
- package/dist/cli/restart-monitor.js +25 -1
- package/dist/cli/stats.js +6 -4
- package/dist/cli/trigger-command.js +146 -25
- package/dist/cli/version.js +6 -1
- package/dist/config/access-policy-domain.js +38 -0
- package/dist/config/access-policy.js +134 -0
- package/dist/config/builtin-role-templates.js +42 -17
- package/dist/config/builtin-roles.js +31 -6
- package/dist/config/config-field-policy.js +17 -5
- package/dist/config/config-manager.js +409 -29
- package/dist/config/config-operation-service.js +35 -38
- package/dist/config/contact-alias.js +68 -0
- package/dist/config/contact-bind-code.js +274 -0
- package/dist/config/contact-book-store.js +622 -0
- package/dist/config/contact-book-v2-startup.js +35 -0
- package/dist/config/contact-book.js +185 -300
- package/dist/config/contact-operation-service.js +116 -0
- package/dist/config/contact-request-service.js +331 -0
- package/dist/config/peer-role-resolver.js +136 -55
- package/dist/config/role-ranks.js +18 -0
- package/dist/config/role-service.js +16 -19
- package/dist/config/role-store.js +16 -5
- package/dist/config/roles.js +10 -1
- package/dist/config/schema-registry.js +49 -24
- package/dist/config-store.js +14 -4
- package/dist/core/auth/agent-delegation.js +105 -11
- package/dist/core/auth/authorization-audit.js +16 -1
- package/dist/core/auth/operation-authorizer.js +10 -0
- package/dist/core/auth/operation-catalog.js +105 -1
- package/dist/core/auth/trigger-authorization.js +15 -0
- package/dist/core/bootstrap-service.js +2 -9
- package/dist/core/channel-loader.js +6 -2
- package/dist/core/command/command-handler.js +113 -21
- package/dist/core/command/connect-menu.js +599 -0
- package/dist/core/command/evol-menu-version-gate.js +38 -0
- package/dist/core/command/group-menu.js +421 -0
- package/dist/core/command/menu-handler.js +382 -100
- package/dist/core/command/menu-protocol.js +8 -2
- package/dist/core/command/menu-token-store.js +102 -0
- package/dist/core/command/role-menu.js +128 -29
- package/dist/core/command/slash-gate.js +1 -1
- package/dist/core/command/slash-handler.js +186 -49
- package/dist/core/daemon-file-cache.js +40 -6
- package/dist/core/event-catalog.js +99 -0
- package/dist/core/evolagent-registry.js +4 -40
- package/dist/core/evolagent.js +20 -9
- package/dist/core/handoff/runtime.js +162 -37
- package/dist/core/handoff/store.js +46 -2
- package/dist/core/handoff/types.js +1 -0
- package/dist/core/inference/text-inference.js +16 -74
- package/dist/core/message/file-markers.js +7 -0
- package/dist/core/message/im-renderer.js +20 -14
- package/dist/core/message/inbound-admission.js +124 -0
- package/dist/core/message/message-bridge.js +670 -57
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-queue.js +195 -10
- package/dist/core/message/message-utils.js +8 -2
- package/dist/core/message/peer-mode.js +7 -8
- package/dist/core/message/response-engine.js +642 -96
- package/dist/core/message/send-receipt.js +24 -0
- package/dist/core/message/stream-debouncer.js +11 -2
- package/dist/core/permission/approval-gateway.js +17 -10
- package/dist/core/permission/ec-command-parser.js +101 -46
- package/dist/core/permission/tool-policy.js +115 -38
- package/dist/core/protected-paths.js +38 -11
- package/dist/core/session/session-fs-store.js +44 -3
- package/dist/core/session/session-manager.js +86 -8
- package/dist/core/session/session-mapper.js +2 -0
- package/dist/core/session/session-renew.js +125 -69
- package/dist/core/session/session-turn-coordinator.js +5 -1
- package/dist/eck/kit-renderer.js +12 -1
- package/dist/eck/message-renderer.js +79 -1
- package/dist/index.js +285 -94
- package/dist/ipc.js +114 -8
- package/dist/paths.js +3 -0
- package/dist/response-system/config-resolver.js +29 -0
- package/dist/response-system/coordinator.js +8 -32
- package/dist/response-system/engines/v1/proactive-flow.js +1 -1
- package/dist/response-system/index.js +1 -0
- package/dist/response-system/modes/single-session/index.js +7 -4
- package/dist/stats/billing.js +20 -8
- package/dist/trigger/manager.js +3 -0
- package/dist/trigger/parser.js +126 -11
- package/dist/trigger/patch.js +12 -2
- package/dist/trigger/scheduler.js +444 -40
- package/dist/trigger/validation.js +13 -0
- package/dist/utils/aid-bind.js +43 -29
- package/dist/utils/error-dict.json +7 -0
- package/dist/utils/evolcore-version.js +21 -0
- package/dist/utils/instance-registry.js +14 -7
- package/dist/utils/log-writer.js +46 -0
- package/dist/utils/logger.js +2 -2
- package/dist/utils/media-cache.js +4 -1
- package/dist/utils/model-prices.jsonl +6 -3
- package/dist/utils/restart-safety.js +31 -0
- package/dist/utils/stable-semver.js +21 -0
- package/dist/utils/stats.js +33 -9
- package/dist/utils/system-memory.js +62 -0
- package/kits/docs/INDEX.md +2 -1
- package/kits/docs/channels/aun.md +4 -13
- package/kits/docs/evolcore/INDEX.md +5 -3
- package/kits/docs/evolcore/agent.md +9 -1
- package/kits/docs/evolcore/aid.md +5 -2
- package/kits/docs/evolcore/config.md +47 -2
- package/kits/docs/evolcore/contact.md +61 -0
- package/kits/docs/evolcore/fs.md +9 -0
- package/kits/docs/evolcore/group-rules.md +46 -4
- package/kits/docs/evolcore/group.md +15 -6
- package/kits/docs/evolcore/model.md +4 -1
- package/kits/docs/evolcore/msg.md +12 -6
- package/kits/docs/evolcore/response.md +16 -21
- package/kits/docs/evolcore/rpc.md +2 -0
- package/kits/docs/evolcore/stats.md +15 -2
- package/kits/docs/evolcore/storage.md +1 -0
- package/kits/docs/evolcore/trigger.md +42 -10
- package/kits/docs/path-registry.md +36 -17
- package/kits/eck_manifest.json +12 -0
- package/kits/eck_message_manifest.json +12 -1
- package/kits/migrations/migrate-contact-book-v2.mjs +754 -0
- package/kits/rules/01-overview.md +17 -7
- package/kits/rules/02-navigation.md +38 -18
- package/kits/rules/03-identity.md +28 -24
- package/kits/rules/04-relation.md +44 -28
- package/kits/rules/05-venue.md +31 -15
- package/kits/rules/06-channel.md +11 -7
- package/kits/schemas/_meta.json +21 -7
- package/kits/schemas/agent-config.schema.6.json +322 -0
- package/kits/schemas/agent-config.schema.7.json +303 -0
- package/kits/schemas/agent-config.schema.8.json +304 -0
- package/kits/schemas/agent-config.schema.9.json +374 -0
- package/kits/schemas/contact-book.schema.2.json +43 -0
- package/kits/schemas/contact-book.schema.3.json +67 -0
- package/kits/schemas/daemon.schema.1.json +3 -2
- package/kits/schemas/daemon.schema.2.json +101 -0
- package/kits/schemas/daemon.schema.3.json +123 -0
- package/kits/schemas/defaults.schema.2.json +85 -0
- package/kits/schemas/defaults.schema.3.json +73 -0
- package/kits/schemas/relation-config.schema.5.json +47 -0
- package/kits/schemas/relation-config.schema.6.json +46 -0
- package/kits/schemas/relation-config.schema.7.json +59 -0
- package/kits/schemas/role-config.schema.1.json +1 -0
- package/kits/schemas/role-registry.schema.1.json +2 -2
- package/kits/schemas/single-session.schema.2.json +57 -0
- package/kits/templates/message-fragments/handoff-context-to-target.md +9 -0
- package/kits/templates/message-fragments/handoff-request-to-target.md +14 -8
- package/kits/templates/message-fragments/handoff-response-to-origin.md +5 -6
- package/kits/templates/roles/admin.json +47 -0
- package/kits/templates/roles/member.json +5 -0
- package/kits/templates/roles/owner.json +1 -0
- package/kits/templates/roles/visitor.json +5 -0
- package/kits/templates/system-fragments/channel.md +12 -2
- package/kits/templates/system-fragments/commands.md +3 -1
- package/kits/templates/system-fragments/identity.md +3 -1
- package/kits/templates/system-fragments/relation.md +1 -1
- package/kits/templates/system-fragments/session.md +6 -2
- package/package.json +7 -5
- package/MIGRATION-0.5.0.md +0 -378
- package/ROLE_ACCESS_CONTROL.md +0 -174
- package/assets/brand/evolcore/README.md +0 -19
- package/assets/brand/evolcore/evolcore-app-icon.png +0 -0
- package/assets/brand/evolcore/evolcore-app-icon.svg +0 -13
- package/assets/brand/evolcore/evolcore-brand-board.png +0 -0
- package/assets/brand/evolcore/evolcore-brand-board.svg +0 -126
- package/assets/brand/evolcore/evolcore-logo-kit.zip +0 -0
- package/assets/brand/evolcore/evolcore-logo-reverse.png +0 -0
- package/assets/brand/evolcore/evolcore-logo-reverse.svg +0 -14
- package/assets/brand/evolcore/evolcore-logo.png +0 -0
- package/assets/brand/evolcore/evolcore-logo.svg +0 -14
- package/assets/brand/evolcore/evolcore-mark.png +0 -0
- 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
|
+
}
|