evolcore 0.0.3 → 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 +64 -0
- 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 +1 -31
- package/dist/agents/codex-app-server-client.js +68 -0
- package/dist/agents/codex-runner.js +157 -5
- 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/channels/aun.js +220 -112
- package/dist/channels/daemon.js +18 -4
- package/dist/channels/dingtalk.js +52 -137
- package/dist/channels/feishu.js +56 -4
- package/dist/channels/qqbot.js +23 -1
- package/dist/channels/wechat.js +303 -155
- package/dist/channels/wecom.js +383 -7
- package/dist/cli/aun-commands.js +11 -11
- package/dist/cli/handoff-command.js +2 -1
- package/dist/cli/init-channel.js +185 -67
- package/dist/cli/init.js +7 -5
- package/dist/cli/trigger-command.js +92 -11
- package/dist/config/access-policy-domain.js +38 -0
- package/dist/config/access-policy.js +134 -0
- package/dist/config/builtin-role-templates.js +27 -14
- package/dist/config/builtin-roles.js +25 -6
- package/dist/config/config-field-policy.js +17 -5
- package/dist/config/config-manager.js +198 -22
- package/dist/config/config-operation-service.js +35 -38
- package/dist/config/contact-bind-code.js +274 -0
- package/dist/config/contact-book-store.js +173 -5
- package/dist/config/contact-book.js +32 -0
- package/dist/config/contact-operation-service.js +9 -3
- package/dist/config/contact-request-service.js +331 -0
- package/dist/config/peer-role-resolver.js +3 -1
- package/dist/config/schema-registry.js +49 -24
- package/dist/config-store.js +14 -2
- package/dist/core/auth/agent-delegation.js +105 -11
- package/dist/core/auth/authorization-audit.js +6 -0
- package/dist/core/auth/operation-authorizer.js +8 -0
- package/dist/core/auth/operation-catalog.js +51 -3
- 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 +10 -13
- package/dist/core/command/connect-menu.js +249 -24
- 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 +269 -85
- package/dist/core/command/menu-protocol.js +8 -2
- package/dist/core/command/menu-token-store.js +102 -0
- package/dist/core/command/slash-gate.js +1 -1
- package/dist/core/command/slash-handler.js +158 -31
- package/dist/core/daemon-file-cache.js +28 -0
- package/dist/core/event-catalog.js +29 -0
- package/dist/core/evolagent-registry.js +4 -39
- 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 +18 -14
- package/dist/core/message/inbound-admission.js +124 -0
- package/dist/core/message/message-bridge.js +616 -74
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-queue.js +185 -10
- package/dist/core/message/peer-mode.js +7 -8
- package/dist/core/message/response-engine.js +381 -79
- 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 +69 -24
- package/dist/core/protected-paths.js +36 -11
- package/dist/core/session/session-manager.js +25 -3
- 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 +224 -89
- package/dist/ipc.js +81 -4
- 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/trigger/parser.js +55 -15
- package/dist/trigger/patch.js +4 -1
- package/dist/trigger/scheduler.js +441 -39
- package/dist/utils/error-dict.json +7 -0
- package/dist/utils/evolcore-version.js +21 -0
- package/dist/utils/logger.js +2 -2
- package/dist/utils/stable-semver.js +21 -0
- package/dist/utils/stats.js +33 -9
- package/kits/docs/channels/aun.md +4 -13
- package/kits/docs/evolcore/INDEX.md +1 -1
- package/kits/docs/evolcore/config.md +47 -2
- package/kits/docs/evolcore/contact.md +7 -3
- package/kits/docs/evolcore/group-rules.md +46 -4
- package/kits/docs/evolcore/group.md +4 -4
- package/kits/docs/evolcore/msg.md +5 -5
- package/kits/docs/evolcore/trigger.md +25 -8
- package/kits/docs/path-registry.md +36 -17
- package/kits/eck_message_manifest.json +12 -1
- package/kits/migrations/migrate-contact-book-v2.mjs +7 -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 +18 -7
- 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.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.6.json +46 -0
- package/kits/schemas/relation-config.schema.7.json +59 -0
- 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 +46 -0
- package/kits/templates/roles/member.json +4 -0
- package/kits/templates/roles/visitor.json +4 -0
- package/kits/templates/system-fragments/channel.md +12 -2
- 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 +4 -2
- package/MIGRATION-0.5.0.md +0 -378
- package/ROLE_ACCESS_CONTROL.md +0 -174
- package/dist/channels/contact-bind-code.js +0 -134
- package/dist/channels/wecom-card.js +0 -101
- package/dist/channels/wecom-onboarding.js +0 -82
- package/dist/channels/wecom-state.js +0 -191
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { roleMenuQuery, roleMenuUpdate, } from './role-menu.js';
|
|
2
2
|
import { getContactSnapshot, resolveContactView } from '../../config/contact-book.js';
|
|
3
|
-
import {
|
|
3
|
+
import { listConfiguredChannelKeys, mutateAccessPolicy, readAccessPolicySnapshot, } from '../../config/access-policy.js';
|
|
4
|
+
import { expirePendingContactRequests, mutateContactWithOperation, reviewContactRequest, } from '../../config/contact-request-service.js';
|
|
4
5
|
import { formatPeerKey, parsePeerKey } from '../relation/peer-identity.js';
|
|
5
6
|
/**
|
|
6
7
|
* Connect Menu — 联系人与关系管理协议
|
|
@@ -10,8 +11,10 @@ import { formatPeerKey, parsePeerKey } from '../relation/peer-identity.js';
|
|
|
10
11
|
* 复用其 agent 级互斥锁、CAS revision、owner/admin 安全护栏与审计链,绝不自行写 relation 文件。
|
|
11
12
|
* - 群组列表(view=groups)→ 委派给 role-menu 的 view=targets(数据源是 AUN live 目录),
|
|
12
13
|
* 群组不落 contact.json(contact book 只接受个人 AID,group 会被拒绝)。
|
|
13
|
-
* -
|
|
14
|
-
* contact.json
|
|
14
|
+
* - 联系人与申请(view=contacts / contact,add / block / unblock / review)→ 直接读写
|
|
15
|
+
* Contact Book v3;contact.json 是当前状态和 pending 版本的唯一事实来源。
|
|
16
|
+
* - 访问设置(view=access / option=allowedGroupChannels / resource=<access field>)→
|
|
17
|
+
* 查询策略、列举群聊渠道候选项并逐字段写入 Agent config.json 的 access 字段。
|
|
15
18
|
*/
|
|
16
19
|
const MENU_NAME = 'connect';
|
|
17
20
|
const DEFAULT_PAGE_SIZE = 50;
|
|
@@ -19,6 +22,7 @@ const MAX_PAGE_SIZE = 200;
|
|
|
19
22
|
/**
|
|
20
23
|
* Map a connect menu request to its authorization operation id.
|
|
21
24
|
* Read views/options → connect.read; contact writes → connect.write;
|
|
25
|
+
* owner-only access fields → connect.access.write;
|
|
22
26
|
* role assignment/admin delegate to the role.* operations role-menu enforces.
|
|
23
27
|
*/
|
|
24
28
|
export function connectMenuOperation(kind, args, value) {
|
|
@@ -27,6 +31,11 @@ export function connectMenuOperation(kind, args, value) {
|
|
|
27
31
|
if (kind === 'action')
|
|
28
32
|
return 'connect.write';
|
|
29
33
|
const resource = String(args?.resource ?? '');
|
|
34
|
+
if (resource === 'allowedGroupChannels')
|
|
35
|
+
return 'connect.write';
|
|
36
|
+
if (resource === 'policyMode' || resource === 'requestLimit') {
|
|
37
|
+
return 'connect.access.write';
|
|
38
|
+
}
|
|
30
39
|
if (resource === 'admin')
|
|
31
40
|
return 'connect.admin';
|
|
32
41
|
const decoded = decodeUpdateValue(value);
|
|
@@ -41,14 +50,12 @@ export async function handleConnectMenu(req, context) {
|
|
|
41
50
|
const { self } = context;
|
|
42
51
|
if (!self)
|
|
43
52
|
return fail('INVALID_ARGUMENT', 'selfAid is required for connect menu');
|
|
44
|
-
if (req.args?.self !== self) {
|
|
45
|
-
return fail(
|
|
46
|
-
? 'args.self does not match the receiving Agent'
|
|
47
|
-
: 'args.self is required for connect menu');
|
|
53
|
+
if (req.args?.self !== undefined && req.args.self !== self) {
|
|
54
|
+
return fail('PERMISSION_DENIED', 'args.self does not match the receiving Agent');
|
|
48
55
|
}
|
|
49
56
|
try {
|
|
50
57
|
if (req.subtype === 'options')
|
|
51
|
-
return await handleOptions(self);
|
|
58
|
+
return await handleOptions(self, req.args);
|
|
52
59
|
if (req.subtype === 'query')
|
|
53
60
|
return await handleQuery(req, self, context);
|
|
54
61
|
if (req.subtype === 'update')
|
|
@@ -62,22 +69,43 @@ export async function handleConnectMenu(req, context) {
|
|
|
62
69
|
return fail('INVALID_ARGUMENT', `Unknown subtype: ${req.subtype}`);
|
|
63
70
|
}
|
|
64
71
|
// ── Options ──
|
|
65
|
-
async function handleOptions(self) {
|
|
72
|
+
async function handleOptions(self, args) {
|
|
73
|
+
const option = String(args?.option ?? '').trim();
|
|
74
|
+
if (option) {
|
|
75
|
+
if (option !== 'allowedGroupChannels') {
|
|
76
|
+
return fail('NOT_SUPPORTED', `Unknown connect option: ${option}`);
|
|
77
|
+
}
|
|
78
|
+
const selected = new Set(readAccessPolicySnapshot(self).policy.allowedGroupChannels);
|
|
79
|
+
return ok({
|
|
80
|
+
options: listConfiguredChannelKeys(self).map(channelKey => ({
|
|
81
|
+
option: channelKey,
|
|
82
|
+
label: channelKey,
|
|
83
|
+
selected: selected.has(channelKey),
|
|
84
|
+
})),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
await expirePendingContactRequests(self);
|
|
66
88
|
const snapshot = getContactSnapshot(self);
|
|
67
89
|
const contactCount = snapshot.contacts.size;
|
|
68
90
|
const blockedCount = snapshot.blockedIndex.size;
|
|
91
|
+
const pendingCount = [...snapshot.contacts.values()].filter(entry => entry.status === 'pending').length;
|
|
69
92
|
return ok({
|
|
70
93
|
options: [
|
|
71
94
|
{
|
|
72
95
|
option: 'contacts',
|
|
73
96
|
label: '联系人列表',
|
|
74
|
-
description:
|
|
97
|
+
description: `查看所有状态(${contactCount} 人,${pendingCount} 个待审核,${blockedCount} 人已拉黑)`,
|
|
75
98
|
},
|
|
76
99
|
{
|
|
77
100
|
option: 'groups',
|
|
78
101
|
label: '群组列表',
|
|
79
102
|
description: '查看所在群组',
|
|
80
103
|
},
|
|
104
|
+
{
|
|
105
|
+
option: 'access',
|
|
106
|
+
label: '访问设置',
|
|
107
|
+
description: '设置私聊准入模式与允许群聊的渠道',
|
|
108
|
+
},
|
|
81
109
|
],
|
|
82
110
|
});
|
|
83
111
|
}
|
|
@@ -89,6 +117,8 @@ async function handleQuery(req, self, context) {
|
|
|
89
117
|
return queryContacts(args, self);
|
|
90
118
|
if (view === 'contact')
|
|
91
119
|
return queryContact(req, self);
|
|
120
|
+
if (view === 'access')
|
|
121
|
+
return queryAccess(self);
|
|
92
122
|
// Groups and role assignments are delegated to role-menu (authoritative source).
|
|
93
123
|
// Role *definitions* are out of scope here — use `menu name=role` for those.
|
|
94
124
|
if (view === 'groups') {
|
|
@@ -124,9 +154,19 @@ async function handleQuery(req, self, context) {
|
|
|
124
154
|
dataSource: { ...data.dataSource, kind: 'relation-config' },
|
|
125
155
|
});
|
|
126
156
|
}
|
|
127
|
-
return fail('
|
|
157
|
+
return fail('NOT_SUPPORTED', `Unknown view: ${String(view)}`);
|
|
128
158
|
}
|
|
129
|
-
function
|
|
159
|
+
function queryAccess(self) {
|
|
160
|
+
const { policy, accessRevision } = readAccessPolicySnapshot(self);
|
|
161
|
+
return ok({
|
|
162
|
+
self,
|
|
163
|
+
view: 'access',
|
|
164
|
+
policy,
|
|
165
|
+
accessRevision,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
async function queryContacts(args, self) {
|
|
169
|
+
await expirePendingContactRequests(self);
|
|
130
170
|
const snapshot = getContactSnapshot(self);
|
|
131
171
|
const page = paginate([...snapshot.contacts.keys()].sort(), args, self, primaryId => primaryId);
|
|
132
172
|
const items = page.items.map(primaryId => contactItem(self, primaryId));
|
|
@@ -135,11 +175,13 @@ function queryContacts(args, self) {
|
|
|
135
175
|
view: 'contacts',
|
|
136
176
|
items,
|
|
137
177
|
contactRevision: snapshot.contactRevision,
|
|
178
|
+
pendingCount: [...snapshot.contacts.values()].filter(entry => entry.status === 'pending').length,
|
|
138
179
|
nextCursor: page.nextCursor,
|
|
139
180
|
dataSource: { kind: 'contact-book', authoritative: true, complete: true },
|
|
140
181
|
});
|
|
141
182
|
}
|
|
142
|
-
function queryContact(req, self) {
|
|
183
|
+
async function queryContact(req, self) {
|
|
184
|
+
await expirePendingContactRequests(self);
|
|
143
185
|
const primaryId = String(req.args?.primaryId ?? '').trim();
|
|
144
186
|
if (!primaryId)
|
|
145
187
|
return fail('INVALID_ARGUMENT', 'primaryId is required');
|
|
@@ -150,6 +192,7 @@ function queryContact(req, self) {
|
|
|
150
192
|
}
|
|
151
193
|
function contactItem(self, primaryId) {
|
|
152
194
|
const view = resolveContactView(self, primaryId);
|
|
195
|
+
const pendingRequest = getContactSnapshot(self).contacts.get(primaryId)?.pendingRequest;
|
|
153
196
|
return {
|
|
154
197
|
primaryId,
|
|
155
198
|
displayName: view.displayName ?? null,
|
|
@@ -157,6 +200,13 @@ function contactItem(self, primaryId) {
|
|
|
157
200
|
blocked: view.blocked,
|
|
158
201
|
isOwner: view.isOwner,
|
|
159
202
|
aliases: view.aliases,
|
|
203
|
+
...(pendingRequest ? {
|
|
204
|
+
pendingRequest: {
|
|
205
|
+
id: pendingRequest.id,
|
|
206
|
+
submittedAt: pendingRequest.submittedAt,
|
|
207
|
+
expiresAt: pendingRequest.expiresAt,
|
|
208
|
+
},
|
|
209
|
+
} : {}),
|
|
160
210
|
};
|
|
161
211
|
}
|
|
162
212
|
function normalizeAssignmentItem(item) {
|
|
@@ -181,12 +231,15 @@ function normalizeAssignmentItem(item) {
|
|
|
181
231
|
return item;
|
|
182
232
|
}
|
|
183
233
|
}
|
|
184
|
-
// ── Update
|
|
234
|
+
// ── Update ──
|
|
185
235
|
async function handleUpdate(req, context) {
|
|
186
236
|
const args = req.args ?? {};
|
|
187
|
-
const resource = args.resource;
|
|
237
|
+
const resource = String(args.resource ?? '');
|
|
238
|
+
if (isAccessUpdateResource(resource)) {
|
|
239
|
+
return updateAccessResource(req, context, resource);
|
|
240
|
+
}
|
|
188
241
|
if (resource !== 'assignment' && resource !== 'admin') {
|
|
189
|
-
return fail('
|
|
242
|
+
return fail('NOT_SUPPORTED', `Unknown resource: ${String(resource)}`);
|
|
190
243
|
}
|
|
191
244
|
if (req.value === undefined || !req.value.trim()) {
|
|
192
245
|
return fail('INVALID_ARGUMENT', 'value is required');
|
|
@@ -194,9 +247,9 @@ async function handleUpdate(req, context) {
|
|
|
194
247
|
validateWritableTarget(args.target);
|
|
195
248
|
const value = roleMenuUpdateValue(resource, req.value);
|
|
196
249
|
const data = await roleMenuUpdate(context, args, value);
|
|
197
|
-
return ok(data);
|
|
250
|
+
return ok(resource === 'assignment' ? normalizeAssignmentUpdate(data) : data);
|
|
198
251
|
}
|
|
199
|
-
// ── Action (Contact Book
|
|
252
|
+
// ── Action (Contact Book v3) ──
|
|
200
253
|
async function handleAction(req, self, context) {
|
|
201
254
|
const action = req.action;
|
|
202
255
|
if (action === 'add')
|
|
@@ -205,9 +258,111 @@ async function handleAction(req, self, context) {
|
|
|
205
258
|
return actionSetStatus(req, self, context, true);
|
|
206
259
|
if (action === 'unblock')
|
|
207
260
|
return actionSetStatus(req, self, context, false);
|
|
208
|
-
|
|
261
|
+
if (action === 'approve-request')
|
|
262
|
+
return actionReviewRequest(req, self, context, 'approve');
|
|
263
|
+
if (action === 'reject-request')
|
|
264
|
+
return actionReviewRequest(req, self, context, 'reject');
|
|
265
|
+
if (action === 'block-request')
|
|
266
|
+
return actionReviewRequest(req, self, context, 'block');
|
|
267
|
+
return fail('NOT_SUPPORTED', `Unknown action: ${String(action)}`);
|
|
268
|
+
}
|
|
269
|
+
function isAccessUpdateResource(resource) {
|
|
270
|
+
return resource === 'policyMode'
|
|
271
|
+
|| resource === 'allowedGroupChannels'
|
|
272
|
+
|| resource === 'requestLimit';
|
|
273
|
+
}
|
|
274
|
+
function updateAccessResource(req, context, resource) {
|
|
275
|
+
const args = req.args ?? {};
|
|
276
|
+
if (resource === 'allowedGroupChannels')
|
|
277
|
+
requireAdminContext(context);
|
|
278
|
+
else
|
|
279
|
+
requireOwnerContext(context);
|
|
280
|
+
if (req.value === undefined || !req.value.trim()) {
|
|
281
|
+
return fail('INVALID_ARGUMENT', 'value is required');
|
|
282
|
+
}
|
|
283
|
+
const self = context.self;
|
|
284
|
+
const expectedAccessRevision = requireAccessRevision(args);
|
|
285
|
+
const decoded = decodeAccessUpdateValue(req.value);
|
|
286
|
+
if (resource === 'policyMode') {
|
|
287
|
+
if (typeof decoded !== 'string' || !['open', 'contacts', 'owners-only'].includes(decoded)) {
|
|
288
|
+
return fail('INVALID_ARGUMENT', 'policyMode must be open, contacts, or owners-only');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
else if (resource === 'allowedGroupChannels') {
|
|
292
|
+
if (!Array.isArray(decoded)
|
|
293
|
+
|| !decoded.every(value => typeof value === 'string' && value.length > 0)
|
|
294
|
+
|| new Set(decoded).size !== decoded.length) {
|
|
295
|
+
return fail('INVALID_ARGUMENT', 'allowedGroupChannels must be a JSON string array');
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
const requestLimit = decoded;
|
|
300
|
+
if (!requestLimit || typeof requestLimit !== 'object' || Array.isArray(requestLimit)
|
|
301
|
+
|| Object.keys(requestLimit).some(key => key !== 'maxTimes' && key !== 'coolDown')
|
|
302
|
+
|| !Number.isInteger(requestLimit.maxTimes)
|
|
303
|
+
|| Number(requestLimit.maxTimes) < 1
|
|
304
|
+
|| Number(requestLimit.maxTimes) > 100_000
|
|
305
|
+
|| !Number.isInteger(requestLimit.coolDown)
|
|
306
|
+
|| Number(requestLimit.coolDown) < 1
|
|
307
|
+
|| Number(requestLimit.coolDown) > 86_400) {
|
|
308
|
+
return fail('INVALID_ARGUMENT', 'requestLimit must be a JSON object with integer maxTimes and coolDown');
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const result = mutateAccessPolicy({
|
|
312
|
+
selfAid: self,
|
|
313
|
+
actorAid: String(context.actorAid || ''),
|
|
314
|
+
actorRole: context.actorRole,
|
|
315
|
+
resource: `access.${resource}`,
|
|
316
|
+
expectedAccessRevision,
|
|
317
|
+
update: current => accessPolicyWithUpdate(current, resource, decoded),
|
|
318
|
+
});
|
|
319
|
+
return ok({
|
|
320
|
+
self,
|
|
321
|
+
resource,
|
|
322
|
+
policy: result.policy,
|
|
323
|
+
accessRevision: result.accessRevision,
|
|
324
|
+
changed: result.changed,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
async function actionReviewRequest(req, self, context, decision) {
|
|
328
|
+
const args = req.args ?? {};
|
|
329
|
+
const primaryId = String(args.primaryId ?? '').trim();
|
|
330
|
+
const requestId = String(args.requestId ?? '').trim();
|
|
331
|
+
if (!primaryId)
|
|
332
|
+
return fail('INVALID_ARGUMENT', 'primaryId is required');
|
|
333
|
+
if (!requestId)
|
|
334
|
+
return fail('INVALID_ARGUMENT', 'requestId is required');
|
|
335
|
+
requireAdminContext(context);
|
|
336
|
+
const expectedContactRevision = requireContactRevision(args);
|
|
337
|
+
const actorId = String(context.actorAid || '').trim();
|
|
338
|
+
const reviewed = await reviewContactRequest({
|
|
339
|
+
selfAid: self,
|
|
340
|
+
primaryId,
|
|
341
|
+
requestId,
|
|
342
|
+
expectedContactRevision,
|
|
343
|
+
approverId: actorId,
|
|
344
|
+
actorId,
|
|
345
|
+
reviewerRole: context.actorRole,
|
|
346
|
+
decision,
|
|
347
|
+
});
|
|
348
|
+
if (reviewed.code === 'approved' || reviewed.code === 'rejected' || reviewed.code === 'blocked') {
|
|
349
|
+
return ok({
|
|
350
|
+
self,
|
|
351
|
+
action: `${decision}-request`,
|
|
352
|
+
primaryId,
|
|
353
|
+
requestId,
|
|
354
|
+
result: reviewed.code,
|
|
355
|
+
contactRevision: reviewed.contactRevision,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
const code = reviewed.code === 'forbidden' ? 'PERMISSION_DENIED'
|
|
359
|
+
: reviewed.code === 'revision-conflict' ? 'CONFLICT'
|
|
360
|
+
: reviewed.code === 'request-expired' ? 'EXPIRED'
|
|
361
|
+
: 'NOT_FOUND';
|
|
362
|
+
return fail(code, reviewed.code.replace(/-/g, ' '), { contactRevision: reviewed.contactRevision });
|
|
209
363
|
}
|
|
210
364
|
async function actionAdd(req, self, context) {
|
|
365
|
+
requireAdminContext(context);
|
|
211
366
|
const args = req.args ?? {};
|
|
212
367
|
const primaryId = String(args.primaryId ?? '').trim();
|
|
213
368
|
if (!primaryId)
|
|
@@ -219,11 +374,13 @@ async function actionAdd(req, self, context) {
|
|
|
219
374
|
}
|
|
220
375
|
const current = getContactSnapshot(self).contacts.get(primaryId);
|
|
221
376
|
const displayName = hasDisplayName ? args.displayName : current?.displayName ?? null;
|
|
222
|
-
const result = await
|
|
377
|
+
const result = await mutateContactWithOperation({
|
|
223
378
|
selfAid: self,
|
|
224
|
-
|
|
379
|
+
actorId: context.actorAid ?? 'connect-menu',
|
|
380
|
+
operation: 'manual-add',
|
|
225
381
|
expectedContactRevision,
|
|
226
382
|
mutation: { type: 'set-display-name', primaryId, displayName },
|
|
383
|
+
primaryId,
|
|
227
384
|
});
|
|
228
385
|
const storedDisplayName = result.contact.contacts[primaryId]?.displayName ?? null;
|
|
229
386
|
return ok({
|
|
@@ -236,16 +393,22 @@ async function actionAdd(req, self, context) {
|
|
|
236
393
|
});
|
|
237
394
|
}
|
|
238
395
|
async function actionSetStatus(req, self, context, blocked) {
|
|
396
|
+
requireAdminContext(context);
|
|
239
397
|
const args = req.args ?? {};
|
|
240
398
|
const primaryId = String(args.primaryId ?? '').trim();
|
|
241
399
|
if (!primaryId)
|
|
242
400
|
return fail('INVALID_ARGUMENT', 'primaryId is required');
|
|
243
401
|
const expectedContactRevision = requireContactRevision(args);
|
|
244
|
-
|
|
402
|
+
if (!getContactSnapshot(self).contacts.has(primaryId)) {
|
|
403
|
+
return fail('NOT_FOUND', `Contact was not found: ${primaryId}`);
|
|
404
|
+
}
|
|
405
|
+
const result = await mutateContactWithOperation({
|
|
245
406
|
selfAid: self,
|
|
246
|
-
|
|
407
|
+
actorId: context.actorAid ?? 'connect-menu',
|
|
408
|
+
operation: blocked ? 'block' : 'unblock',
|
|
247
409
|
expectedContactRevision,
|
|
248
410
|
mutation: { type: 'set-status', primaryId, status: blocked ? 'blocked' : 'active' },
|
|
411
|
+
primaryId,
|
|
249
412
|
});
|
|
250
413
|
return ok({
|
|
251
414
|
self,
|
|
@@ -259,6 +422,16 @@ async function actionSetStatus(req, self, context, blocked) {
|
|
|
259
422
|
function ok(data) {
|
|
260
423
|
return { type: 'menu.response', id: '', name: MENU_NAME, data };
|
|
261
424
|
}
|
|
425
|
+
function requireOwnerContext(context) {
|
|
426
|
+
if (context.actorRole !== 'owner') {
|
|
427
|
+
throw connectError('PERMISSION_DENIED', 'Only an Agent Owner can modify this access setting');
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
function requireAdminContext(context) {
|
|
431
|
+
if (context.actorRole !== 'owner' && context.actorRole !== 'admin') {
|
|
432
|
+
throw connectError('PERMISSION_DENIED', 'Only an Agent Owner or Admin can perform this operation');
|
|
433
|
+
}
|
|
434
|
+
}
|
|
262
435
|
function fail(code, message, data) {
|
|
263
436
|
return {
|
|
264
437
|
type: 'menu.response',
|
|
@@ -277,6 +450,20 @@ function errorCode(error) {
|
|
|
277
450
|
return 'CONFLICT';
|
|
278
451
|
if (code === 'INVALID_CONTACT_BOOK')
|
|
279
452
|
return 'INTERNAL_ERROR';
|
|
453
|
+
if (code === 'access_policy_owner_required')
|
|
454
|
+
return 'PERMISSION_DENIED';
|
|
455
|
+
if (code === 'access_policy_manager_required')
|
|
456
|
+
return 'PERMISSION_DENIED';
|
|
457
|
+
if (code === 'access_policy_revision_conflict')
|
|
458
|
+
return 'CONFLICT';
|
|
459
|
+
if (code === 'access_policy_temporarily_unavailable')
|
|
460
|
+
return 'TEMPORARILY_UNAVAILABLE';
|
|
461
|
+
if (code === 'restricted_mode_without_owner')
|
|
462
|
+
return 'NOT_ALLOWED';
|
|
463
|
+
if (code === 'unknown_group_access_channel')
|
|
464
|
+
return 'INVALID_ARGUMENT';
|
|
465
|
+
if (code === 'invalid_access_policy')
|
|
466
|
+
return 'INTERNAL_ERROR';
|
|
280
467
|
return typeof code === 'string' ? code : 'INTERNAL_ERROR';
|
|
281
468
|
}
|
|
282
469
|
function errorMessage(error) {
|
|
@@ -297,6 +484,15 @@ function requireContactRevision(args) {
|
|
|
297
484
|
}
|
|
298
485
|
return args.expectedContactRevision;
|
|
299
486
|
}
|
|
487
|
+
function requireAccessRevision(args) {
|
|
488
|
+
if (!Object.prototype.hasOwnProperty.call(args, 'expectedAccessRevision')) {
|
|
489
|
+
throw connectError('INVALID_ARGUMENT', 'expectedAccessRevision is required');
|
|
490
|
+
}
|
|
491
|
+
if (typeof args.expectedAccessRevision !== 'string' || !args.expectedAccessRevision) {
|
|
492
|
+
throw connectError('INVALID_ARGUMENT', 'expectedAccessRevision must be a non-empty string');
|
|
493
|
+
}
|
|
494
|
+
return args.expectedAccessRevision;
|
|
495
|
+
}
|
|
300
496
|
function decodeUpdateValue(value) {
|
|
301
497
|
const text = String(value ?? '').trim();
|
|
302
498
|
if (!text)
|
|
@@ -309,6 +505,15 @@ function decodeUpdateValue(value) {
|
|
|
309
505
|
catch { }
|
|
310
506
|
return text;
|
|
311
507
|
}
|
|
508
|
+
function decodeAccessUpdateValue(value) {
|
|
509
|
+
const text = value.trim();
|
|
510
|
+
try {
|
|
511
|
+
return JSON.parse(text);
|
|
512
|
+
}
|
|
513
|
+
catch {
|
|
514
|
+
return text;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
312
517
|
function roleMenuUpdateValue(resource, value) {
|
|
313
518
|
const decoded = decodeUpdateValue(value);
|
|
314
519
|
if (resource === 'assignment') {
|
|
@@ -317,11 +522,31 @@ function roleMenuUpdateValue(resource, value) {
|
|
|
317
522
|
}
|
|
318
523
|
return JSON.stringify(decoded);
|
|
319
524
|
}
|
|
320
|
-
if (
|
|
525
|
+
if (decoded !== 'grant' && decoded !== 'revoke') {
|
|
321
526
|
throw connectError('INVALID_ARGUMENT', 'admin value must be grant or revoke');
|
|
322
527
|
}
|
|
323
528
|
return JSON.stringify(decoded);
|
|
324
529
|
}
|
|
530
|
+
function normalizeAssignmentUpdate(data) {
|
|
531
|
+
const explicitRole = data?.assignment?.explicitRole;
|
|
532
|
+
return {
|
|
533
|
+
...data,
|
|
534
|
+
roleId: typeof explicitRole === 'string' ? explicitRole : null,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
function accessPolicyWithUpdate(current, resource, decoded) {
|
|
538
|
+
if (resource === 'policyMode') {
|
|
539
|
+
return { ...current, policyMode: decoded };
|
|
540
|
+
}
|
|
541
|
+
if (resource === 'allowedGroupChannels') {
|
|
542
|
+
return { ...current, allowedGroupChannels: [...decoded] };
|
|
543
|
+
}
|
|
544
|
+
const requestLimit = decoded;
|
|
545
|
+
return {
|
|
546
|
+
...current,
|
|
547
|
+
requestLimit: { maxTimes: requestLimit.maxTimes, coolDown: requestLimit.coolDown },
|
|
548
|
+
};
|
|
549
|
+
}
|
|
325
550
|
function validateWritableTarget(target) {
|
|
326
551
|
if (!target || typeof target !== 'object' || Array.isArray(target))
|
|
327
552
|
return;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { loadDaemonConfig } from '../../config-store.js';
|
|
2
|
+
import { readInstalledEvolcoreVersion } from '../../utils/evolcore-version.js';
|
|
3
|
+
import { compareStableSemver, parseStableSemver } from '../../utils/stable-semver.js';
|
|
4
|
+
export function evaluateEvolMenuVersionGate(input) {
|
|
5
|
+
const minimum = loadDaemonConfig().aun?.minEvolVersion;
|
|
6
|
+
if (!minimum)
|
|
7
|
+
return null;
|
|
8
|
+
const minimumVersion = parseStableSemver(minimum);
|
|
9
|
+
if (!minimumVersion) {
|
|
10
|
+
throw new Error('daemon.json.aun.minEvolVersion must use stable X.Y.Z format');
|
|
11
|
+
}
|
|
12
|
+
const received = input.protectedHeaders?.evol_version;
|
|
13
|
+
const receivedVersion = input.encrypted ? parseStableSemver(received) : null;
|
|
14
|
+
if (!receivedVersion || compareStableSemver(receivedVersion, minimumVersion) < 0) {
|
|
15
|
+
return {
|
|
16
|
+
code: 'UPGRADE_REQUIRED',
|
|
17
|
+
message: `Evol ${minimum} or later is required`,
|
|
18
|
+
data: {
|
|
19
|
+
minimum,
|
|
20
|
+
received: receivedVersion ? received : null,
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
export function isEvolMenuVersionGateEnabled() {
|
|
27
|
+
return loadDaemonConfig().aun?.minEvolVersion !== undefined;
|
|
28
|
+
}
|
|
29
|
+
/** Token may be issued while disabled, but only this flag makes it an access requirement. */
|
|
30
|
+
export function isAunMenuTokenRequired() {
|
|
31
|
+
return loadDaemonConfig().aun?.menuTokenRequired === true;
|
|
32
|
+
}
|
|
33
|
+
export function evolMenuResponseTransportMetadata(_requestEncrypted) {
|
|
34
|
+
return {
|
|
35
|
+
encrypted: true,
|
|
36
|
+
protectedHeaders: { ec_version: readInstalledEvolcoreVersion() },
|
|
37
|
+
};
|
|
38
|
+
}
|