evolcore 0.0.2 → 0.0.3
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 +44 -793
- package/dist/agents/claude-runner.js +197 -17
- package/dist/agents/codex-runner.js +46 -3
- package/dist/aun/outbox.js +8 -0
- package/dist/channels/aun.js +21 -4
- package/dist/channels/contact-bind-code.js +134 -0
- package/dist/channels/dingtalk.js +979 -149
- package/dist/channels/feishu.js +130 -54
- package/dist/channels/wecom-card.js +101 -0
- package/dist/channels/wecom-onboarding.js +82 -0
- package/dist/channels/wecom-state.js +191 -0
- package/dist/channels/wecom.js +755 -163
- package/dist/cli/agent-command.js +2 -1
- package/dist/cli/aun-commands.js +88 -33
- 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 +2 -2
- package/dist/cli/help.js +9 -5
- package/dist/cli/index.js +132 -116
- package/dist/cli/init-channel.js +92 -97
- package/dist/cli/init.js +63 -26
- 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 +55 -15
- package/dist/cli/version.js +6 -1
- package/dist/config/builtin-role-templates.js +22 -10
- package/dist/config/builtin-roles.js +7 -1
- package/dist/config/config-manager.js +221 -17
- package/dist/config/contact-alias.js +68 -0
- package/dist/config/contact-book-store.js +454 -0
- package/dist/config/contact-book-v2-startup.js +35 -0
- package/dist/config/contact-book.js +156 -303
- package/dist/config/contact-operation-service.js +110 -0
- package/dist/config/peer-role-resolver.js +133 -54
- 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-store.js +0 -2
- package/dist/core/auth/authorization-audit.js +10 -1
- package/dist/core/auth/operation-authorizer.js +2 -0
- package/dist/core/auth/operation-catalog.js +56 -0
- package/dist/core/command/command-handler.js +105 -10
- package/dist/core/command/connect-menu.js +374 -0
- package/dist/core/command/menu-handler.js +114 -16
- package/dist/core/command/role-menu.js +128 -29
- package/dist/core/command/slash-handler.js +28 -18
- package/dist/core/daemon-file-cache.js +12 -6
- package/dist/core/event-catalog.js +70 -0
- package/dist/core/evolagent-registry.js +0 -1
- package/dist/core/evolagent.js +20 -9
- package/dist/core/message/im-renderer.js +2 -0
- package/dist/core/message/message-bridge.js +79 -8
- package/dist/core/message/message-queue.js +10 -0
- package/dist/core/message/message-utils.js +8 -2
- package/dist/core/message/response-engine.js +269 -25
- package/dist/core/message/send-receipt.js +24 -0
- package/dist/core/message/stream-debouncer.js +11 -2
- package/dist/core/permission/tool-policy.js +47 -15
- package/dist/core/protected-paths.js +2 -0
- package/dist/core/session/session-fs-store.js +44 -3
- package/dist/core/session/session-manager.js +61 -5
- package/dist/index.js +62 -6
- package/dist/ipc.js +34 -5
- package/dist/stats/billing.js +20 -8
- package/dist/trigger/manager.js +3 -0
- package/dist/trigger/parser.js +77 -2
- package/dist/trigger/patch.js +8 -1
- package/dist/trigger/scheduler.js +3 -1
- package/dist/trigger/validation.js +13 -0
- package/dist/utils/aid-bind.js +43 -29
- package/dist/utils/instance-registry.js +14 -7
- package/dist/utils/log-writer.js +46 -0
- 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/system-memory.js +62 -0
- package/kits/docs/INDEX.md +2 -1
- 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/contact.md +57 -0
- package/kits/docs/evolcore/fs.md +9 -0
- package/kits/docs/evolcore/group.md +12 -3
- package/kits/docs/evolcore/model.md +4 -1
- package/kits/docs/evolcore/msg.md +9 -3
- 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 +17 -2
- package/kits/eck_manifest.json +12 -0
- package/kits/migrations/migrate-contact-book-v2.mjs +747 -0
- package/kits/schemas/_meta.json +7 -4
- package/kits/schemas/agent-config.schema.6.json +322 -0
- package/kits/schemas/contact-book.schema.2.json +43 -0
- package/kits/schemas/relation-config.schema.5.json +47 -0
- package/kits/schemas/role-config.schema.1.json +1 -0
- package/kits/schemas/role-registry.schema.1.json +2 -2
- package/kits/templates/roles/admin.json +1 -0
- package/kits/templates/roles/member.json +1 -0
- package/kits/templates/roles/owner.json +1 -0
- package/kits/templates/roles/visitor.json +1 -0
- package/kits/templates/system-fragments/commands.md +3 -1
- package/package.json +4 -4
- 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
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { agentContactConfig } from '../paths.js';
|
|
2
2
|
import { isValidAid } from '../aun/aid/validation.js';
|
|
3
|
-
import {
|
|
3
|
+
import { isExplicitGroupId } from '../aun/group-identity.js';
|
|
4
|
+
import { ConfigTarget, read, validateConfig } from './config-manager.js';
|
|
5
|
+
import { ContactMutationError, contactRevision, mutateContactBookSync } from './contact-book-store.js';
|
|
6
|
+
import { formatContactAlias, normalizeContactChannelKey, parseContactAlias, } from './contact-alias.js';
|
|
4
7
|
export class ContactBookError extends Error {
|
|
5
8
|
code;
|
|
6
9
|
constructor(code, message) {
|
|
@@ -10,13 +13,11 @@ export class ContactBookError extends Error {
|
|
|
10
13
|
}
|
|
11
14
|
}
|
|
12
15
|
let aliasMapCache = new WeakMap();
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
conflictingLegacyAliases: new Set(),
|
|
16
|
+
let snapshotByBook = new WeakMap();
|
|
17
|
+
const EMPTY_ALIAS_MAP = {
|
|
18
|
+
aliasToPrimary: new Map(),
|
|
19
|
+
conflictingAliases: new Set(),
|
|
18
20
|
};
|
|
19
|
-
const SCOPED_ALIAS_PREFIX = 'channel-key:';
|
|
20
21
|
export function agentContactConfigPath(aid) {
|
|
21
22
|
return agentContactConfig(aid);
|
|
22
23
|
}
|
|
@@ -26,346 +27,198 @@ export function readContactBook(selfAid, opts = {}) {
|
|
|
26
27
|
return read(ConfigTarget.Contact, { self: selfAid }, { cache: opts.cache ?? true });
|
|
27
28
|
}
|
|
28
29
|
export function resolvePrincipal(selfAid, channelType, actorId, channelKey) {
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
if (
|
|
32
|
-
return isValidAid(
|
|
33
|
-
? { principalId:
|
|
34
|
-
: { principalStatus:
|
|
35
|
-
}
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
const legacyAlias = normalizeLegacyAlias(normalizedChannel, normalizedActor);
|
|
39
|
-
if (!isValidAid(normalizedSelf) || !normalizedChannelKey || !legacyAlias) {
|
|
30
|
+
const actor = String(actorId || '').trim();
|
|
31
|
+
const type = String(channelType || '').trim().toLowerCase();
|
|
32
|
+
if (type === 'aun') {
|
|
33
|
+
return isValidAid(actor)
|
|
34
|
+
? { principalId: actor, principalStatus: 'native_aid' }
|
|
35
|
+
: { principalStatus: actor ? 'unmapped' : 'error' };
|
|
36
|
+
}
|
|
37
|
+
const canonicalChannelKey = normalizeContactChannelKey(selfAid, type, channelKey);
|
|
38
|
+
if (!isValidAid(String(selfAid || '').trim()) || !actor || !canonicalChannelKey) {
|
|
40
39
|
return { principalStatus: 'error' };
|
|
41
40
|
}
|
|
42
|
-
const scopedAlias = scopedAliasLookupKey(normalizedChannelKey, normalizedActor);
|
|
43
41
|
try {
|
|
44
|
-
const aliases = loadAliasMap(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
const scopedPrincipalId = aliases.scopedAliasToPrimary.get(scopedAlias);
|
|
49
|
-
if (scopedPrincipalId) {
|
|
50
|
-
return { principalId: scopedPrincipalId, principalStatus: 'mapped' };
|
|
51
|
-
}
|
|
52
|
-
// Legacy aliases did not identify a channel instance. They remain trusted only
|
|
53
|
-
// while the configured channel type has exactly one matching instance.
|
|
54
|
-
if (!isUnambiguousLegacyChannel(normalizedSelf, normalizedChannel, normalizedChannelKey)) {
|
|
55
|
-
return { principalStatus: 'unmapped' };
|
|
56
|
-
}
|
|
57
|
-
if (aliases.conflictingLegacyAliases.has(legacyAlias)) {
|
|
42
|
+
const aliases = loadAliasMap(selfAid);
|
|
43
|
+
const exact = formatContactAlias(canonicalChannelKey, actor);
|
|
44
|
+
if (aliases.conflictingAliases.has(exact))
|
|
58
45
|
return { principalStatus: 'conflict' };
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
: { principalStatus: 'unmapped' };
|
|
46
|
+
const mapped = aliases.aliasToPrimary.get(exact);
|
|
47
|
+
if (mapped)
|
|
48
|
+
return { principalId: mapped, principalStatus: 'mapped' };
|
|
49
|
+
return { principalStatus: 'unmapped' };
|
|
64
50
|
}
|
|
65
51
|
catch {
|
|
66
52
|
return { principalStatus: 'error' };
|
|
67
53
|
}
|
|
68
54
|
}
|
|
69
55
|
export function resolvePrimaryId(selfAid, channelType, actorId, channelKey) {
|
|
70
|
-
const
|
|
71
|
-
return resolvePrincipal(selfAid, channelType,
|
|
56
|
+
const actor = String(actorId || '').trim();
|
|
57
|
+
return resolvePrincipal(selfAid, channelType, actor, channelKey).principalId ?? actor;
|
|
72
58
|
}
|
|
73
59
|
export function bindContactAlias(selfAid, primaryId, channelType, actorId, channelKey) {
|
|
74
60
|
const primary = normalizePrimaryId(primaryId);
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const contacts = next.contacts ?? {};
|
|
81
|
-
let removedLegacyAlias = false;
|
|
82
|
-
// An explicit scoped bind supersedes every unscoped copy of the same alias.
|
|
83
|
-
// Otherwise unbinding the scoped entry could silently reactivate a legacy
|
|
84
|
-
// mapping owned by another principal.
|
|
85
|
-
for (const [contactId, value] of Object.entries(contacts)) {
|
|
86
|
-
const entry = normalizeContactEntry(value);
|
|
87
|
-
const filtered = entry.aliases.filter(item => normalizeLegacyAliasString(item) !== legacyAlias);
|
|
88
|
-
if (filtered.length !== entry.aliases.length) {
|
|
89
|
-
contacts[contactId] = { ...entry, aliases: filtered };
|
|
90
|
-
removedLegacyAlias = true;
|
|
61
|
+
const type = String(channelType || '').trim().toLowerCase();
|
|
62
|
+
const actor = String(actorId || '').trim();
|
|
63
|
+
if (type === 'aun') {
|
|
64
|
+
if (!isValidAid(actor) || actor !== primary) {
|
|
65
|
+
throw new ContactBookError('NATIVE_AID_REMAP_FORBIDDEN', `Native AID "${actor}" cannot be remapped to "${primary}"`);
|
|
91
66
|
}
|
|
92
|
-
}
|
|
93
|
-
const current = normalizeContactEntry(contacts[primary]);
|
|
94
|
-
const alreadyScoped = current.aliases.some(value => (normalizeScopedAliasString(value, selfAid)?.lookupKey === alias.lookupKey));
|
|
95
|
-
if (alreadyScoped && !removedLegacyAlias) {
|
|
96
67
|
return false;
|
|
97
68
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
69
|
+
const canonicalChannelKey = normalizeContactChannelKey(selfAid, type, channelKey);
|
|
70
|
+
if (!canonicalChannelKey || !actor) {
|
|
71
|
+
throw new ContactBookError('INVALID_ALIAS', `Invalid contact alias: ${String(channelKey)}:${String(actorId)}`);
|
|
72
|
+
}
|
|
73
|
+
const alias = formatContactAlias(canonicalChannelKey, actor);
|
|
74
|
+
try {
|
|
75
|
+
return mutateContactBookSync({
|
|
76
|
+
selfAid,
|
|
77
|
+
mutation: { type: 'bind-alias', primaryId: primary, alias },
|
|
78
|
+
}).changed;
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (error instanceof ContactMutationError)
|
|
82
|
+
throw new ContactBookError(error.code, error.message);
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
107
85
|
}
|
|
108
86
|
export function unbindContactAlias(selfAid, primaryId, channelType, actorId, channelKey) {
|
|
109
87
|
const primary = normalizePrimaryId(primaryId);
|
|
110
|
-
const
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
// Remove the requested scoped binding from the target principal and purge
|
|
116
|
-
// every unscoped copy of the same raw alias. Legacy aliases do not carry a
|
|
117
|
-
// channel-instance identity, so retaining one on another contact would
|
|
118
|
-
// silently reactivate that principal after this scoped binding is revoked.
|
|
119
|
-
for (const [contactId, value] of Object.entries(contacts)) {
|
|
120
|
-
const entry = normalizeContactEntry(value);
|
|
121
|
-
const filtered = entry.aliases.filter(item => {
|
|
122
|
-
const isTargetScoped = contactId.trim() === primary
|
|
123
|
-
&& normalizeScopedAliasString(item, selfAid)?.lookupKey === alias.lookupKey;
|
|
124
|
-
const isLegacyCopy = normalizeLegacyAliasString(item) === legacyAlias;
|
|
125
|
-
return !isTargetScoped && !isLegacyCopy;
|
|
126
|
-
});
|
|
127
|
-
if (filtered.length !== entry.aliases.length) {
|
|
128
|
-
contacts[contactId] = { ...entry, aliases: filtered };
|
|
129
|
-
changed = true;
|
|
130
|
-
}
|
|
88
|
+
const type = String(channelType || '').trim().toLowerCase();
|
|
89
|
+
const actor = String(actorId || '').trim();
|
|
90
|
+
const canonicalChannelKey = normalizeContactChannelKey(selfAid, type, channelKey);
|
|
91
|
+
if (!canonicalChannelKey || type === 'aun' || !actor) {
|
|
92
|
+
throw new ContactBookError('INVALID_ALIAS', `Invalid contact alias: ${String(channelKey)}:${String(actorId)}`);
|
|
131
93
|
}
|
|
132
|
-
|
|
133
|
-
|
|
94
|
+
const alias = formatContactAlias(canonicalChannelKey, actor);
|
|
95
|
+
try {
|
|
96
|
+
return mutateContactBookSync({
|
|
97
|
+
selfAid,
|
|
98
|
+
mutation: { type: 'unbind-alias', primaryId: primary, alias },
|
|
99
|
+
}).changed;
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (error instanceof ContactMutationError)
|
|
103
|
+
throw new ContactBookError(error.code, error.message);
|
|
104
|
+
throw error;
|
|
134
105
|
}
|
|
135
|
-
next.contacts = contacts;
|
|
136
|
-
write(ConfigTarget.Contact, next, { self: selfAid });
|
|
137
|
-
return true;
|
|
138
106
|
}
|
|
139
107
|
export function clearContactBookCache() {
|
|
140
108
|
aliasMapCache = new WeakMap();
|
|
109
|
+
snapshotByBook = new WeakMap();
|
|
110
|
+
}
|
|
111
|
+
export function getContactSnapshot(selfAid) {
|
|
112
|
+
const book = readContactBook(selfAid, { cache: true }) ?? { $schema_version: 2, contacts: {} };
|
|
113
|
+
const cached = snapshotByBook.get(book);
|
|
114
|
+
if (cached)
|
|
115
|
+
return cached;
|
|
116
|
+
const errors = validateConfig(ConfigTarget.Contact, book);
|
|
117
|
+
if (errors.length) {
|
|
118
|
+
throw new ContactBookError('INVALID_CONTACT_BOOK', `Invalid contact book for ${selfAid}: ${errors.join('; ')}`);
|
|
119
|
+
}
|
|
120
|
+
const aliases = buildAliasMap(book, selfAid);
|
|
121
|
+
if (aliases.conflictingAliases.size) {
|
|
122
|
+
throw new ContactBookError('ALIAS_CONFLICT', `Conflicting contact aliases: ${[...aliases.conflictingAliases].join(', ')}`);
|
|
123
|
+
}
|
|
124
|
+
const snapshot = buildSnapshot(book, selfAid, aliases);
|
|
125
|
+
snapshotByBook.set(book, snapshot);
|
|
126
|
+
return snapshot;
|
|
127
|
+
}
|
|
128
|
+
export function resolveContactView(selfAid, primaryId) {
|
|
129
|
+
const id = String(primaryId || '').trim();
|
|
130
|
+
const owner = listAgentOwners(selfAid).includes(id);
|
|
131
|
+
const entry = getContactSnapshot(selfAid).contacts.get(id);
|
|
132
|
+
const status = owner ? 'active' : entry?.status ?? 'active';
|
|
133
|
+
return {
|
|
134
|
+
primaryId: id,
|
|
135
|
+
...(entry?.displayName ? { displayName: entry.displayName } : {}),
|
|
136
|
+
aliases: entry?.aliases ?? [],
|
|
137
|
+
status,
|
|
138
|
+
blocked: status === 'blocked',
|
|
139
|
+
isOwner: owner,
|
|
140
|
+
source: owner ? 'owner' : entry ? 'contact' : 'none',
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
export function isBlockedContact(selfAid, primaryId) {
|
|
144
|
+
return resolveContactView(selfAid, primaryId).blocked;
|
|
145
|
+
}
|
|
146
|
+
function buildSnapshot(book, selfAid, aliases) {
|
|
147
|
+
const contacts = new Map();
|
|
148
|
+
const blockedIndex = new Set();
|
|
149
|
+
const owners = new Set(listAgentOwners(selfAid));
|
|
150
|
+
for (const [primaryId, rawEntry] of Object.entries(book.contacts)) {
|
|
151
|
+
if (!isValidAid(primaryId) || isExplicitGroupId(primaryId)) {
|
|
152
|
+
throw new ContactBookError('INVALID_CONTACT_BOOK', `Invalid contact AID: ${primaryId}`);
|
|
153
|
+
}
|
|
154
|
+
if (rawEntry.status === 'blocked' && owners.has(primaryId)) {
|
|
155
|
+
throw new ContactBookError('INVALID_CONTACT_BOOK', `Owner contact ${primaryId} cannot be blocked`);
|
|
156
|
+
}
|
|
157
|
+
const entry = Object.freeze({
|
|
158
|
+
...rawEntry,
|
|
159
|
+
...(rawEntry.aliases ? { aliases: Object.freeze([...rawEntry.aliases]) } : {}),
|
|
160
|
+
});
|
|
161
|
+
contacts.set(primaryId, entry);
|
|
162
|
+
if (entry.status === 'blocked')
|
|
163
|
+
blockedIndex.add(primaryId);
|
|
164
|
+
}
|
|
165
|
+
return Object.freeze({
|
|
166
|
+
contacts,
|
|
167
|
+
aliasIndex: aliases.aliasToPrimary,
|
|
168
|
+
conflictingAliases: aliases.conflictingAliases,
|
|
169
|
+
blockedIndex,
|
|
170
|
+
contactRevision: contactRevision(book),
|
|
171
|
+
});
|
|
141
172
|
}
|
|
142
173
|
function loadAliasMap(selfAid) {
|
|
143
174
|
const book = readContactBook(selfAid, { cache: true });
|
|
144
175
|
if (!book)
|
|
145
|
-
return
|
|
176
|
+
return EMPTY_ALIAS_MAP;
|
|
146
177
|
const cached = aliasMapCache.get(book);
|
|
147
178
|
if (cached)
|
|
148
179
|
return cached;
|
|
149
|
-
const
|
|
150
|
-
if (
|
|
151
|
-
throw new ContactBookError('INVALID_CONTACT_BOOK', `Invalid contact book for ${selfAid}: ${
|
|
180
|
+
const errors = validateConfig(ConfigTarget.Contact, book);
|
|
181
|
+
if (errors.length) {
|
|
182
|
+
throw new ContactBookError('INVALID_CONTACT_BOOK', `Invalid contact book for ${selfAid}: ${errors.join('; ')}`);
|
|
152
183
|
}
|
|
153
184
|
const built = buildAliasMap(book, selfAid);
|
|
154
185
|
aliasMapCache.set(book, built);
|
|
155
186
|
return built;
|
|
156
187
|
}
|
|
157
|
-
function buildAliasMap(
|
|
158
|
-
const
|
|
159
|
-
const
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
const contacts = config?.contacts;
|
|
163
|
-
if (!contacts || typeof contacts !== 'object' || Array.isArray(contacts)) {
|
|
164
|
-
return {
|
|
165
|
-
scopedAliasToPrimary,
|
|
166
|
-
conflictingScopedAliases,
|
|
167
|
-
legacyAliasToPrimary,
|
|
168
|
-
conflictingLegacyAliases,
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
for (const [rawPrimaryId, entry] of Object.entries(contacts)) {
|
|
172
|
-
const primaryId = rawPrimaryId.trim();
|
|
173
|
-
if (!isValidAid(primaryId) || !entry || typeof entry !== 'object' || Array.isArray(entry))
|
|
188
|
+
function buildAliasMap(book, selfAid) {
|
|
189
|
+
const aliasToPrimary = new Map();
|
|
190
|
+
const conflictingAliases = new Set();
|
|
191
|
+
for (const [primaryId, entry] of Object.entries(book?.contacts ?? {})) {
|
|
192
|
+
if (!isValidAid(primaryId) || isExplicitGroupId(primaryId) || !entry || typeof entry !== 'object')
|
|
174
193
|
continue;
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
194
|
+
for (const rawAlias of entry.aliases ?? []) {
|
|
195
|
+
const parsed = parseContactAlias(rawAlias, selfAid);
|
|
196
|
+
if (!parsed) {
|
|
197
|
+
throw new ContactBookError('INVALID_CONTACT_BOOK', `Invalid contact alias for ${selfAid}: ${String(rawAlias)}`);
|
|
198
|
+
}
|
|
199
|
+
const alias = parsed.alias;
|
|
200
|
+
if (conflictingAliases.has(alias))
|
|
201
|
+
continue;
|
|
202
|
+
const existing = aliasToPrimary.get(alias);
|
|
203
|
+
if (existing && existing !== primaryId) {
|
|
204
|
+
aliasToPrimary.delete(alias);
|
|
205
|
+
conflictingAliases.add(alias);
|
|
180
206
|
}
|
|
181
207
|
else {
|
|
182
|
-
|
|
183
|
-
if (legacy) {
|
|
184
|
-
addAliasMapping(legacyAliasToPrimary, conflictingLegacyAliases, legacy, primaryId);
|
|
185
|
-
}
|
|
208
|
+
aliasToPrimary.set(alias, primaryId);
|
|
186
209
|
}
|
|
187
210
|
}
|
|
188
211
|
}
|
|
189
|
-
return {
|
|
190
|
-
scopedAliasToPrimary,
|
|
191
|
-
conflictingScopedAliases,
|
|
192
|
-
legacyAliasToPrimary,
|
|
193
|
-
conflictingLegacyAliases,
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
function addAliasMapping(aliases, conflicts, alias, primaryId) {
|
|
197
|
-
if (conflicts.has(alias))
|
|
198
|
-
return;
|
|
199
|
-
const existing = aliases.get(alias);
|
|
200
|
-
if (existing && existing !== primaryId) {
|
|
201
|
-
aliases.delete(alias);
|
|
202
|
-
conflicts.add(alias);
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
aliases.set(alias, primaryId);
|
|
206
|
-
}
|
|
207
|
-
function cloneContactBook(config) {
|
|
208
|
-
const next = config ? { ...config } : {};
|
|
209
|
-
const contacts = {};
|
|
210
|
-
const rawContacts = config?.contacts;
|
|
211
|
-
if (rawContacts && typeof rawContacts === 'object' && !Array.isArray(rawContacts)) {
|
|
212
|
-
for (const [primaryId, entry] of Object.entries(rawContacts)) {
|
|
213
|
-
contacts[primaryId] = normalizeContactEntry(entry);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
next.contacts = contacts;
|
|
217
|
-
return next;
|
|
218
|
-
}
|
|
219
|
-
function normalizeContactEntry(value) {
|
|
220
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
221
|
-
return { aliases: [] };
|
|
222
|
-
}
|
|
223
|
-
const entry = value;
|
|
224
|
-
return {
|
|
225
|
-
...entry,
|
|
226
|
-
aliases: Array.isArray(entry.aliases)
|
|
227
|
-
? entry.aliases.filter((item) => typeof item === 'string')
|
|
228
|
-
: [],
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
function ensureScopedAliasAvailable(config, selfAid, primaryId, alias) {
|
|
232
|
-
const owners = findScopedAliasOwners(config, selfAid, alias);
|
|
233
|
-
if (owners.size === 0)
|
|
234
|
-
return;
|
|
235
|
-
if (owners.size === 1 && owners.has(primaryId))
|
|
236
|
-
return;
|
|
237
|
-
throw new ContactBookError('ALIAS_CONFLICT', `Alias "${alias}" is already bound to ${[...owners].join(', ')}`);
|
|
238
|
-
}
|
|
239
|
-
function findScopedAliasOwners(config, selfAid, alias) {
|
|
240
|
-
const owners = new Set();
|
|
241
|
-
const contacts = config.contacts;
|
|
242
|
-
if (!contacts || typeof contacts !== 'object' || Array.isArray(contacts))
|
|
243
|
-
return owners;
|
|
244
|
-
for (const [rawPrimaryId, entry] of Object.entries(contacts)) {
|
|
245
|
-
const primaryId = rawPrimaryId.trim();
|
|
246
|
-
if (!primaryId || !entry || typeof entry !== 'object' || Array.isArray(entry))
|
|
247
|
-
continue;
|
|
248
|
-
const aliases = Array.isArray(entry.aliases) ? entry.aliases : [];
|
|
249
|
-
if (aliases.some(value => normalizeScopedAliasString(value, selfAid)?.lookupKey === alias)) {
|
|
250
|
-
owners.add(primaryId);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
return owners;
|
|
212
|
+
return { aliasToPrimary, conflictingAliases };
|
|
254
213
|
}
|
|
255
214
|
function normalizePrimaryId(value) {
|
|
256
|
-
const
|
|
257
|
-
if (!isValidAid(
|
|
258
|
-
throw new ContactBookError('INVALID_PRIMARY_ID', `Invalid contact
|
|
259
|
-
}
|
|
260
|
-
return primaryId;
|
|
261
|
-
}
|
|
262
|
-
function requireLegacyAlias(channelType, actorId) {
|
|
263
|
-
const alias = normalizeLegacyAlias(channelType, actorId);
|
|
264
|
-
if (!alias) {
|
|
265
|
-
throw new ContactBookError('INVALID_ALIAS', `Invalid contact alias: ${String(channelType)}:${String(actorId)}`);
|
|
266
|
-
}
|
|
267
|
-
return alias;
|
|
268
|
-
}
|
|
269
|
-
function normalizeLegacyAlias(channelType, actorId) {
|
|
270
|
-
const channel = String(channelType || '').trim().toLowerCase();
|
|
271
|
-
const id = String(actorId || '').trim();
|
|
272
|
-
if (!/^[a-z0-9_-]+$/.test(channel) || !id)
|
|
273
|
-
return null;
|
|
274
|
-
return `${channel}:${id}`;
|
|
275
|
-
}
|
|
276
|
-
function normalizeLegacyAliasString(value) {
|
|
277
|
-
if (typeof value !== 'string')
|
|
278
|
-
return null;
|
|
279
|
-
const trimmed = value.trim();
|
|
280
|
-
if (trimmed.startsWith(SCOPED_ALIAS_PREFIX))
|
|
281
|
-
return null;
|
|
282
|
-
const idx = trimmed.indexOf(':');
|
|
283
|
-
if (idx <= 0 || idx === trimmed.length - 1)
|
|
284
|
-
return null;
|
|
285
|
-
return normalizeLegacyAlias(trimmed.slice(0, idx), trimmed.slice(idx + 1));
|
|
286
|
-
}
|
|
287
|
-
function requireScopedAlias(selfAid, channelType, actorId, channelKey) {
|
|
288
|
-
const normalizedActor = String(actorId || '').trim();
|
|
289
|
-
const normalizedChannel = String(channelType || '').trim().toLowerCase();
|
|
290
|
-
const normalizedChannelKey = normalizeChannelKey(selfAid, normalizedChannel, channelKey);
|
|
291
|
-
if (!normalizedActor || !normalizedChannelKey) {
|
|
292
|
-
throw new ContactBookError('INVALID_CHANNEL_ALIAS', `Invalid scoped contact alias: ${String(channelKey)}:${String(actorId)}`);
|
|
293
|
-
}
|
|
294
|
-
return {
|
|
295
|
-
lookupKey: scopedAliasLookupKey(normalizedChannelKey, normalizedActor),
|
|
296
|
-
serialized: serializeScopedAlias(normalizedChannelKey, normalizedActor),
|
|
297
|
-
};
|
|
298
|
-
}
|
|
299
|
-
function normalizeScopedAliasString(value, selfAid) {
|
|
300
|
-
if (typeof value !== 'string')
|
|
301
|
-
return null;
|
|
302
|
-
const trimmed = value.trim();
|
|
303
|
-
if (!trimmed.startsWith(SCOPED_ALIAS_PREFIX))
|
|
304
|
-
return null;
|
|
305
|
-
const encoded = trimmed.slice(SCOPED_ALIAS_PREFIX.length);
|
|
306
|
-
const separator = encoded.indexOf(':');
|
|
307
|
-
if (separator <= 0 || separator === encoded.length - 1)
|
|
308
|
-
return null;
|
|
309
|
-
try {
|
|
310
|
-
const channelKey = decodeURIComponent(encoded.slice(0, separator));
|
|
311
|
-
const actorId = decodeURIComponent(encoded.slice(separator + 1)).trim();
|
|
312
|
-
const channelType = channelKey.split('#', 1)[0]?.toLowerCase() ?? '';
|
|
313
|
-
const normalizedChannelKey = normalizeChannelKey(selfAid, channelType, channelKey);
|
|
314
|
-
if (!normalizedChannelKey || !actorId)
|
|
315
|
-
return null;
|
|
316
|
-
return {
|
|
317
|
-
lookupKey: scopedAliasLookupKey(normalizedChannelKey, actorId),
|
|
318
|
-
serialized: serializeScopedAlias(normalizedChannelKey, actorId),
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
catch {
|
|
322
|
-
return null;
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
function serializeScopedAlias(channelKey, actorId) {
|
|
326
|
-
return `${SCOPED_ALIAS_PREFIX}${encodeURIComponent(channelKey)}:${encodeURIComponent(actorId)}`;
|
|
327
|
-
}
|
|
328
|
-
function scopedAliasLookupKey(channelKey, actorId) {
|
|
329
|
-
return JSON.stringify([channelKey, actorId]);
|
|
330
|
-
}
|
|
331
|
-
function normalizeChannelKey(selfAid, channelType, value) {
|
|
332
|
-
const key = typeof value === 'string' ? value.trim() : '';
|
|
333
|
-
const parts = key.split('#');
|
|
334
|
-
if (parts.length !== 3)
|
|
335
|
-
return null;
|
|
336
|
-
const [rawType, keySelfAid, name] = parts;
|
|
337
|
-
const normalizedType = rawType.toLowerCase();
|
|
338
|
-
if (!/^[a-z0-9_-]+$/.test(normalizedType)
|
|
339
|
-
|| normalizedType !== channelType
|
|
340
|
-
|| keySelfAid !== selfAid
|
|
341
|
-
|| !name) {
|
|
342
|
-
return null;
|
|
215
|
+
const id = String(value || '').trim();
|
|
216
|
+
if (!isValidAid(id) || isExplicitGroupId(id)) {
|
|
217
|
+
throw new ContactBookError('INVALID_PRIMARY_ID', `Invalid individual contact AID: ${String(value)}`);
|
|
343
218
|
}
|
|
344
|
-
return
|
|
219
|
+
return id;
|
|
345
220
|
}
|
|
346
|
-
function
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const matches = new Set();
|
|
350
|
-
for (const candidate of agent?.channels ?? []) {
|
|
351
|
-
if (!candidate || typeof candidate !== 'object')
|
|
352
|
-
continue;
|
|
353
|
-
const candidateType = String(candidate.type || '').trim().toLowerCase();
|
|
354
|
-
const candidateName = String(candidate.name || '').trim();
|
|
355
|
-
if (candidateType !== channelType || !candidateName || candidateName.includes('#'))
|
|
356
|
-
continue;
|
|
357
|
-
matches.add(`${candidateType}#${selfAid}#${candidateName}`);
|
|
358
|
-
}
|
|
359
|
-
return matches.size === 1 && matches.has(channelKey);
|
|
360
|
-
}
|
|
361
|
-
catch {
|
|
362
|
-
return false;
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
function ensureNativeAidIsNotRemapped(primaryId, channelType, actorId) {
|
|
366
|
-
const channel = String(channelType || '').trim().toLowerCase();
|
|
367
|
-
const actor = String(actorId || '').trim();
|
|
368
|
-
if (channel === 'aun' && isValidAid(actor) && actor !== primaryId) {
|
|
369
|
-
throw new ContactBookError('NATIVE_AID_REMAP_FORBIDDEN', `Native AID "${actor}" cannot be remapped to "${primaryId}"`);
|
|
370
|
-
}
|
|
221
|
+
function listAgentOwners(selfAid) {
|
|
222
|
+
const agent = read(ConfigTarget.Agent, { self: selfAid }, { cache: true });
|
|
223
|
+
return [...new Set((agent?.owners ?? []).map(value => String(value || '').trim()).filter(Boolean))];
|
|
371
224
|
}
|
|
@@ -0,0 +1,110 @@
|
|
|
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, mutateContactBook } from './contact-book-store.js';
|
|
7
|
+
export function resolveContactCommand(argv) {
|
|
8
|
+
const args = argv[0] === 'contact' ? argv.slice(1) : [...argv];
|
|
9
|
+
const selfAid = String(argValue(args, '--self') || '').trim();
|
|
10
|
+
if (!isValidAid(selfAid))
|
|
11
|
+
return invalid('INVALID_SELF', '--self must be a valid Agent AID');
|
|
12
|
+
const positional = stripOptions(args);
|
|
13
|
+
const [command, subcommand, ...extra] = positional;
|
|
14
|
+
if (extra.length > 0)
|
|
15
|
+
return invalid('INVALID_ARGUMENT', `Unexpected contact arguments: ${extra.join(' ')}`);
|
|
16
|
+
if (command === 'list') {
|
|
17
|
+
if (subcommand !== undefined || !args.includes('--blocked')) {
|
|
18
|
+
return invalid('INVALID_ARGUMENT', 'Only ec contact list --blocked is currently supported');
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
ok: true,
|
|
22
|
+
command: { kind: 'list-blocked', operationId: 'contact.read', selfAid, canonicalArgv: canonicalArgv(argv) },
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
if (command === 'block' || command === 'unblock') {
|
|
26
|
+
if (!isValidAid(subcommand || '') || isExplicitGroupId(subcommand || '')) {
|
|
27
|
+
return invalid('INVALID_AID', 'primaryId must be a valid individual AID');
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
ok: true,
|
|
31
|
+
command: {
|
|
32
|
+
kind: 'set-blocked',
|
|
33
|
+
operationId: 'contact.block',
|
|
34
|
+
selfAid,
|
|
35
|
+
primaryId: subcommand,
|
|
36
|
+
blocked: command === 'block',
|
|
37
|
+
canonicalArgv: canonicalArgv(argv),
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return invalid('UNKNOWN_COMMAND', `Unknown contact command: ${positional.join(' ')}`);
|
|
42
|
+
}
|
|
43
|
+
export async function executeResolvedContactCommand(command, actor = 'local-cli') {
|
|
44
|
+
if (!fs.existsSync(agentConfig(command.selfAid))) {
|
|
45
|
+
return { ok: false, code: 'AGENT_NOT_FOUND', error: `Agent does not exist: ${command.selfAid}` };
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
if (command.kind === 'list-blocked') {
|
|
49
|
+
const snapshot = getContactSnapshot(command.selfAid);
|
|
50
|
+
return {
|
|
51
|
+
ok: true,
|
|
52
|
+
operation: 'list-blocked',
|
|
53
|
+
selfAid: command.selfAid,
|
|
54
|
+
contactRevision: snapshot.contactRevision,
|
|
55
|
+
items: [...snapshot.blockedIndex].sort().map(id => resolveContactView(command.selfAid, id)),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (command.kind === 'set-blocked') {
|
|
59
|
+
const result = await mutateContactBook({
|
|
60
|
+
selfAid: command.selfAid,
|
|
61
|
+
actor,
|
|
62
|
+
mutation: {
|
|
63
|
+
type: 'set-status',
|
|
64
|
+
primaryId: command.primaryId,
|
|
65
|
+
status: command.blocked ? 'blocked' : 'active',
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
return {
|
|
69
|
+
ok: true,
|
|
70
|
+
operation: 'set-blocked',
|
|
71
|
+
selfAid: command.selfAid,
|
|
72
|
+
primaryId: command.primaryId,
|
|
73
|
+
blocked: command.blocked,
|
|
74
|
+
changed: result.changed,
|
|
75
|
+
contactRevision: result.contactRevision,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return { ok: false, code: 'CONTACT_OPERATION_FAILED', error: 'Unsupported contact operation' };
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (error instanceof ContactMutationError) {
|
|
82
|
+
return { ok: false, code: error.code, error: error.message, ...(error.data ? { data: error.data } : {}) };
|
|
83
|
+
}
|
|
84
|
+
return { ok: false, code: 'CONTACT_OPERATION_FAILED', error: error instanceof Error ? error.message : String(error) };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function argValue(args, flag) {
|
|
88
|
+
const index = args.indexOf(flag);
|
|
89
|
+
return index >= 0 ? args[index + 1] : undefined;
|
|
90
|
+
}
|
|
91
|
+
function stripOptions(args) {
|
|
92
|
+
const result = [];
|
|
93
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
94
|
+
const arg = args[index];
|
|
95
|
+
if (arg === '--self' || arg === '--format') {
|
|
96
|
+
index += 1;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (arg === '--blocked')
|
|
100
|
+
continue;
|
|
101
|
+
result.push(arg);
|
|
102
|
+
}
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
function canonicalArgv(argv) {
|
|
106
|
+
return argv[0] === 'contact' ? [...argv] : ['contact', ...argv];
|
|
107
|
+
}
|
|
108
|
+
function invalid(code, reason) {
|
|
109
|
+
return { ok: false, code, reason };
|
|
110
|
+
}
|