evolcore 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +83 -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/daemon-commands.js +46 -15
- package/dist/cli/handoff-command.js +2 -1
- package/dist/cli/init-channel.js +185 -67
- package/dist/cli/init.js +49 -27
- package/dist/cli/trigger-command.js +92 -11
- package/dist/config/access-policy-domain.js +18 -0
- package/dist/config/access-policy.js +110 -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 +229 -26
- 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/role-menu.js +16 -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 +134 -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-fs-store.js +19 -4
- package/dist/core/session/session-manager.js +232 -4
- 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/cross-platform.js +8 -2
- 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/process-introspect.js +19 -3
- 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 +8 -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 +364 -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
|
@@ -18,6 +18,8 @@ export const USER_PLANE_CAPABILITY_CEILING = {
|
|
|
18
18
|
'chatmode.update',
|
|
19
19
|
'mentionmode.current',
|
|
20
20
|
'mentionmode.update',
|
|
21
|
+
'group.mentionmode.current',
|
|
22
|
+
'group.rulespolicy.current',
|
|
21
23
|
'session.list',
|
|
22
24
|
'session.create',
|
|
23
25
|
'session.rename',
|
|
@@ -415,6 +417,12 @@ function checkConfigFieldPolicy(ctx, policy, resolvedConfigOp) {
|
|
|
415
417
|
if (!resolvedConfigOp.route || (resolvedConfigOp.fieldRule.class !== 'safe-scalar' && resolvedConfigOp.fieldRule.class !== 'safe-readonly-object')) {
|
|
416
418
|
return { ok: false, reason: `Config field ${field} is not available to user roles` };
|
|
417
419
|
}
|
|
420
|
+
if (policy === 'role-overridable-write'
|
|
421
|
+
&& resolvedConfigOp.fieldRule.access === 'owner-only'
|
|
422
|
+
&& ctx.role !== 'owner'
|
|
423
|
+
&& !ctx.isDaemonOwner) {
|
|
424
|
+
return { ok: false, reason: `Config field ${field} can only be changed by an owner` };
|
|
425
|
+
}
|
|
418
426
|
const roleDef = getRoleDefinition(ctx.role, ctx.selfAid);
|
|
419
427
|
const fieldPermission = roleDef
|
|
420
428
|
? resolveRoleFieldPermission(roleDef.permissions || {}, field)
|
|
@@ -133,6 +133,46 @@ const OPERATIONS = [
|
|
|
133
133
|
description: 'Update group mention mode',
|
|
134
134
|
sources: ['slash', 'menu'],
|
|
135
135
|
},
|
|
136
|
+
{
|
|
137
|
+
id: 'group.list',
|
|
138
|
+
category: 'read',
|
|
139
|
+
dangerous: false,
|
|
140
|
+
defaultScopes: ['agent'],
|
|
141
|
+
description: 'List joined AUN groups',
|
|
142
|
+
sources: ['menu'],
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
id: 'group.mentionmode.current',
|
|
146
|
+
category: 'read',
|
|
147
|
+
dangerous: false,
|
|
148
|
+
defaultScopes: ['relation', 'agent'],
|
|
149
|
+
description: 'Read a group mention mode',
|
|
150
|
+
sources: ['menu'],
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
id: 'group.mentionmode.update',
|
|
154
|
+
category: 'write-own',
|
|
155
|
+
dangerous: false,
|
|
156
|
+
defaultScopes: ['relation', 'agent'],
|
|
157
|
+
description: 'Update a group mention mode',
|
|
158
|
+
sources: ['menu'],
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
id: 'group.rulespolicy.current',
|
|
162
|
+
category: 'read',
|
|
163
|
+
dangerous: false,
|
|
164
|
+
defaultScopes: ['relation', 'agent'],
|
|
165
|
+
description: 'Read a group rules loading policy',
|
|
166
|
+
sources: ['menu'],
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
id: 'group.rulespolicy.update',
|
|
170
|
+
category: 'write-own',
|
|
171
|
+
dangerous: false,
|
|
172
|
+
defaultScopes: ['relation', 'agent'],
|
|
173
|
+
description: 'Update a group rules loading policy',
|
|
174
|
+
sources: ['menu'],
|
|
175
|
+
},
|
|
136
176
|
{
|
|
137
177
|
id: 'session.list',
|
|
138
178
|
category: 'read',
|
|
@@ -180,7 +220,7 @@ const OPERATIONS = [
|
|
|
180
220
|
dangerous: false,
|
|
181
221
|
defaultScopes: ['filesystem'],
|
|
182
222
|
description: '获取文件内容(需路径沙箱检查)',
|
|
183
|
-
sources: ['menu'],
|
|
223
|
+
sources: ['slash', 'menu'],
|
|
184
224
|
},
|
|
185
225
|
// ── Trigger Operations ──
|
|
186
226
|
{
|
|
@@ -317,7 +357,7 @@ const OPERATIONS = [
|
|
|
317
357
|
category: 'write-agent',
|
|
318
358
|
dangerous: false,
|
|
319
359
|
defaultScopes: ['agent'],
|
|
320
|
-
description: 'Block or unblock
|
|
360
|
+
description: 'Block or unblock a canonical contact across bound private channels',
|
|
321
361
|
sources: ['agent-tool'],
|
|
322
362
|
},
|
|
323
363
|
{
|
|
@@ -357,7 +397,15 @@ const OPERATIONS = [
|
|
|
357
397
|
category: 'write-agent',
|
|
358
398
|
dangerous: false,
|
|
359
399
|
defaultScopes: ['agent'],
|
|
360
|
-
description: '
|
|
400
|
+
description: 'Manage contacts and contact requests',
|
|
401
|
+
sources: ['menu', 'ecweb', 'control'],
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
id: 'connect.access.write',
|
|
405
|
+
category: 'write-agent',
|
|
406
|
+
dangerous: false,
|
|
407
|
+
defaultScopes: ['agent'],
|
|
408
|
+
description: 'Manage owner-only Agent access policy and request limit settings',
|
|
361
409
|
sources: ['menu', 'ecweb', 'control'],
|
|
362
410
|
},
|
|
363
411
|
// Agent Operations
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const CROSS_AGENT_READ_OPERATIONS = new Set([
|
|
2
|
+
'trigger.list',
|
|
3
|
+
'trigger.show',
|
|
4
|
+
'trigger.history',
|
|
5
|
+
]);
|
|
6
|
+
export function isCrossAgentTriggerOperationAllowed(input) {
|
|
7
|
+
if (input.control || input.taskAgentAid === input.targetAgentAid)
|
|
8
|
+
return true;
|
|
9
|
+
if (!input.taskAgentAid || !input.targetManagement)
|
|
10
|
+
return false;
|
|
11
|
+
return CROSS_AGENT_READ_OPERATIONS.has(input.operation);
|
|
12
|
+
}
|
|
13
|
+
export function isCrossAgentTriggerReadOperation(operation) {
|
|
14
|
+
return CROSS_AGENT_READ_OPERATIONS.has(operation);
|
|
15
|
+
}
|
|
@@ -2,6 +2,7 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { randomBytes } from 'crypto';
|
|
4
4
|
import { kitsTemplatesDir, agentMdPath } from '../paths.js';
|
|
5
|
+
import { resolveAgentDisplayName } from '../aun/aid/agentmd.js';
|
|
5
6
|
import { logger } from '../utils/logger.js';
|
|
6
7
|
import { loadAgent, saveAgent } from '../config-store.js';
|
|
7
8
|
import { normalizeAgentLifecycle, withLifecycleForWrite } from '../config/lifecycle.js';
|
|
@@ -130,15 +131,7 @@ export class BootstrapService {
|
|
|
130
131
|
return renderTemplate(template, vars).trim();
|
|
131
132
|
}
|
|
132
133
|
resolveAgentDisplayName(aid) {
|
|
133
|
-
|
|
134
|
-
const content = fs.readFileSync(agentMdPath(aid), 'utf-8');
|
|
135
|
-
const fm = content.match(/^---\n([\s\S]*?)\n---/)?.[1] || '';
|
|
136
|
-
const name = fm.match(/^name:\s*["']?(.+?)["']?\s*$/m)?.[1]?.trim();
|
|
137
|
-
if (name)
|
|
138
|
-
return name;
|
|
139
|
-
}
|
|
140
|
-
catch { }
|
|
141
|
-
return aid.split('.')[0];
|
|
134
|
+
return resolveAgentDisplayName(aid) || aid.split('.')[0];
|
|
142
135
|
}
|
|
143
136
|
resolveBaseagent(agent, aid) {
|
|
144
137
|
try {
|
|
@@ -26,7 +26,11 @@ export function showActivitiesPolicy(mode, chatType, _identity) {
|
|
|
26
26
|
}
|
|
27
27
|
// ── ChannelLoader ──────────────────────────────────────────────────────────
|
|
28
28
|
export class ChannelLoader {
|
|
29
|
+
processDebug;
|
|
29
30
|
plugins = new Map();
|
|
31
|
+
constructor(processDebug) {
|
|
32
|
+
this.processDebug = processDebug;
|
|
33
|
+
}
|
|
30
34
|
register(plugin) {
|
|
31
35
|
if (this.plugins.has(plugin.name)) {
|
|
32
36
|
throw new Error(`Channel plugin '${plugin.name}' already registered`);
|
|
@@ -46,7 +50,7 @@ export class ChannelLoader {
|
|
|
46
50
|
const ctx = {
|
|
47
51
|
agentName: agent.aid,
|
|
48
52
|
defaultProjectPath: agent.config.projects?.defaultPath ?? process.cwd(),
|
|
49
|
-
debug:
|
|
53
|
+
debug: this.processDebug,
|
|
50
54
|
};
|
|
51
55
|
// Build the full list of config instances to create.
|
|
52
56
|
// AUN is synthesised from agent.aid; any explicit aun entry in channels[] is skipped.
|
|
@@ -252,7 +256,7 @@ export function buildReloadHooks(deps) {
|
|
|
252
256
|
const ctx = {
|
|
253
257
|
agentName: agent.aid ?? agent.config?.aid,
|
|
254
258
|
defaultProjectPath: agent.config?.projects?.defaultPath ?? process.cwd(),
|
|
255
|
-
debug:
|
|
259
|
+
debug: channelLoader.processDebug,
|
|
256
260
|
};
|
|
257
261
|
const plugin = channelLoader.getPlugin(cfgInst.type);
|
|
258
262
|
if (!plugin)
|
|
@@ -814,12 +814,9 @@ export class CommandHandler {
|
|
|
814
814
|
item.lastActive = s.updatedAt;
|
|
815
815
|
return item;
|
|
816
816
|
}
|
|
817
|
-
/**
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
*/
|
|
821
|
-
getMenuItems(role, chatType = 'private', scope = 'agent') {
|
|
822
|
-
return menuGetMenuItems.call(this, role, chatType, scope);
|
|
817
|
+
/** 返回结构化命令菜单,已迁移的命令按当前授权主体过滤。 */
|
|
818
|
+
getMenuItems(role, chatType = 'private', scope = 'agent', authSubject) {
|
|
819
|
+
return menuGetMenuItems.call(this, role, chatType, scope, authSubject);
|
|
823
820
|
}
|
|
824
821
|
/** 动态子菜单:根据 cmd 路径返回选项列表(供 menu.query + cmd 使用) */
|
|
825
822
|
async getSubMenuItems(cmd, channel, channelId, userId, args, overrideIdentity, explicitChatType, fromControlChannel = false, authSubject, source = 'menu') {
|
|
@@ -843,8 +840,8 @@ export class CommandHandler {
|
|
|
843
840
|
return await menuExecMenuUpdate.call(this, cmd, value, channel, channelId, userId, overrideIdentity, fromControlChannel, args, authSubject, source);
|
|
844
841
|
}
|
|
845
842
|
/** menu.action — 触发动词。 */
|
|
846
|
-
async execMenuAction(cmd, action, args, channel, channelId, userId, overrideIdentity, explicitChatType, requestId, fromControlChannel = false, authSubject, source = 'menu') {
|
|
847
|
-
return await menuExecMenuAction.call(this, cmd, action, args, channel, channelId, userId, overrideIdentity, explicitChatType, requestId, fromControlChannel, authSubject, source);
|
|
843
|
+
async execMenuAction(cmd, action, args, channel, channelId, userId, overrideIdentity, explicitChatType, requestId, fromControlChannel = false, authSubject, source = 'menu', transportReplyContext, deferSideEffect) {
|
|
844
|
+
return await menuExecMenuAction.call(this, cmd, action, args, channel, channelId, userId, overrideIdentity, explicitChatType, requestId, fromControlChannel, authSubject, source, transportReplyContext, deferSideEffect);
|
|
848
845
|
}
|
|
849
846
|
/** ECWeb 专用入口:身份只取可信 IPC 信封。 */
|
|
850
847
|
async execMenuForEcweb(payload, auth) {
|
|
@@ -1168,7 +1165,7 @@ export class CommandHandler {
|
|
|
1168
1165
|
/trigger list [--all] — 查看所有触发器
|
|
1169
1166
|
/trigger create <参数> — 创建触发器(set 为兼容别名)
|
|
1170
1167
|
/trigger update <名称|ID> <参数> — 修改触发器
|
|
1171
|
-
常用参数:--delay/--at/--cron/--every、--prompt、--model、--effort、--permission(省略则继承;update 可用 inherit 清除覆盖)
|
|
1168
|
+
常用参数:--exec script|trigger-session|target-session(create 必填)、--delay/--at/--cron/--every、--prompt、--model、--effort、--permission(省略则继承;update 可用 inherit 清除覆盖)
|
|
1172
1169
|
/trigger enable <名称|ID> — 启用触发器
|
|
1173
1170
|
/trigger disable <名称|ID> — 暂停触发器
|
|
1174
1171
|
/trigger show <名称|ID> — 查看触发器详情
|
|
@@ -1451,11 +1448,11 @@ export class CommandHandler {
|
|
|
1451
1448
|
* Agent managed CLI config entrypoint. Identity and relation are resolved
|
|
1452
1449
|
* from the daemon-owned session rather than caller-provided flags.
|
|
1453
1450
|
*/
|
|
1454
|
-
async handleConfigOperation(argv, sessionId, delegationToken) {
|
|
1451
|
+
async handleConfigOperation(argv, sessionId, delegationToken, delegationCommandHash) {
|
|
1455
1452
|
if (!this.agentDelegationRegistry) {
|
|
1456
1453
|
return { ok: false, code: 'DELEGATION_REQUIRED', error: 'Task delegation is not configured' };
|
|
1457
1454
|
}
|
|
1458
|
-
const delegation = this.agentDelegationRegistry.validate(delegationToken, sessionId);
|
|
1455
|
+
const delegation = this.agentDelegationRegistry.validate(delegationToken, sessionId, delegationCommandHash);
|
|
1459
1456
|
if (!delegation.ok)
|
|
1460
1457
|
return { ok: false, code: delegation.code, error: delegation.reason };
|
|
1461
1458
|
const session = await this.sessionManager.getSessionById(sessionId);
|
|
@@ -1513,11 +1510,11 @@ export class CommandHandler {
|
|
|
1513
1510
|
: { ok: false, code: result.code, error: result.error, ...(result.data !== undefined ? { data: result.data } : {}) };
|
|
1514
1511
|
}
|
|
1515
1512
|
/** Agent-managed contact CLI entrypoint. The daemon owns actor resolution and authorization. */
|
|
1516
|
-
async handleContactOperation(argv, sessionId, delegationToken) {
|
|
1513
|
+
async handleContactOperation(argv, sessionId, delegationToken, delegationCommandHash) {
|
|
1517
1514
|
if (!this.agentDelegationRegistry) {
|
|
1518
1515
|
return { ok: false, code: 'DELEGATION_REQUIRED', error: 'Task delegation is not configured' };
|
|
1519
1516
|
}
|
|
1520
|
-
const delegation = this.agentDelegationRegistry.validate(delegationToken, sessionId);
|
|
1517
|
+
const delegation = this.agentDelegationRegistry.validate(delegationToken, sessionId, delegationCommandHash);
|
|
1521
1518
|
if (!delegation.ok)
|
|
1522
1519
|
return { ok: false, code: delegation.code, error: delegation.reason };
|
|
1523
1520
|
const session = await this.sessionManager.getSessionById(sessionId);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { roleMenuQuery, roleMenuUpdate, } from './role-menu.js';
|
|
1
|
+
import { roleMenuQuery, roleMenuGroupCount, roleMenuUpdate, } from './role-menu.js';
|
|
2
2
|
import { getContactSnapshot, resolveContactView } from '../../config/contact-book.js';
|
|
3
|
-
import {
|
|
3
|
+
import { 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 / 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,9 @@ 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 === 'policyMode' || resource === 'requestLimit') {
|
|
35
|
+
return 'connect.access.write';
|
|
36
|
+
}
|
|
30
37
|
if (resource === 'admin')
|
|
31
38
|
return 'connect.admin';
|
|
32
39
|
const decoded = decodeUpdateValue(value);
|
|
@@ -41,14 +48,12 @@ export async function handleConnectMenu(req, context) {
|
|
|
41
48
|
const { self } = context;
|
|
42
49
|
if (!self)
|
|
43
50
|
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');
|
|
51
|
+
if (req.args?.self !== undefined && req.args.self !== self) {
|
|
52
|
+
return fail('PERMISSION_DENIED', 'args.self does not match the receiving Agent');
|
|
48
53
|
}
|
|
49
54
|
try {
|
|
50
55
|
if (req.subtype === 'options')
|
|
51
|
-
return await handleOptions(self);
|
|
56
|
+
return await handleOptions(self, req.args, context);
|
|
52
57
|
if (req.subtype === 'query')
|
|
53
58
|
return await handleQuery(req, self, context);
|
|
54
59
|
if (req.subtype === 'update')
|
|
@@ -62,21 +67,40 @@ export async function handleConnectMenu(req, context) {
|
|
|
62
67
|
return fail('INVALID_ARGUMENT', `Unknown subtype: ${req.subtype}`);
|
|
63
68
|
}
|
|
64
69
|
// ── Options ──
|
|
65
|
-
async function handleOptions(self) {
|
|
70
|
+
async function handleOptions(self, args, context) {
|
|
71
|
+
const option = String(args?.option ?? '').trim();
|
|
72
|
+
if (option) {
|
|
73
|
+
return fail('NOT_SUPPORTED', `Unknown connect option: ${option}`);
|
|
74
|
+
}
|
|
75
|
+
await expirePendingContactRequests(self);
|
|
66
76
|
const snapshot = getContactSnapshot(self);
|
|
67
77
|
const contactCount = snapshot.contacts.size;
|
|
68
78
|
const blockedCount = snapshot.blockedIndex.size;
|
|
79
|
+
const pendingCount = [...snapshot.contacts.values()].filter(entry => entry.status === 'pending').length;
|
|
80
|
+
let groupsDescription = '查看所在群组(数量暂不可用)';
|
|
81
|
+
try {
|
|
82
|
+
const groupCount = await roleMenuGroupCount(context);
|
|
83
|
+
groupsDescription = `查看所在群组(${groupCount} 个)`;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Keep the root menu available when AUN group directory is disabled or unavailable.
|
|
87
|
+
}
|
|
69
88
|
return ok({
|
|
70
89
|
options: [
|
|
71
90
|
{
|
|
72
91
|
option: 'contacts',
|
|
73
92
|
label: '联系人列表',
|
|
74
|
-
description:
|
|
93
|
+
description: `查看所有状态(${contactCount} 人,${pendingCount} 个待审核,${blockedCount} 人已拉黑)`,
|
|
75
94
|
},
|
|
76
95
|
{
|
|
77
96
|
option: 'groups',
|
|
78
97
|
label: '群组列表',
|
|
79
|
-
description:
|
|
98
|
+
description: groupsDescription,
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
option: 'access',
|
|
102
|
+
label: '访问设置',
|
|
103
|
+
description: '设置消息准入模式和准入控制消息限额',
|
|
80
104
|
},
|
|
81
105
|
],
|
|
82
106
|
});
|
|
@@ -89,6 +113,8 @@ async function handleQuery(req, self, context) {
|
|
|
89
113
|
return queryContacts(args, self);
|
|
90
114
|
if (view === 'contact')
|
|
91
115
|
return queryContact(req, self);
|
|
116
|
+
if (view === 'access')
|
|
117
|
+
return queryAccess(self);
|
|
92
118
|
// Groups and role assignments are delegated to role-menu (authoritative source).
|
|
93
119
|
// Role *definitions* are out of scope here — use `menu name=role` for those.
|
|
94
120
|
if (view === 'groups') {
|
|
@@ -124,9 +150,19 @@ async function handleQuery(req, self, context) {
|
|
|
124
150
|
dataSource: { ...data.dataSource, kind: 'relation-config' },
|
|
125
151
|
});
|
|
126
152
|
}
|
|
127
|
-
return fail('
|
|
153
|
+
return fail('NOT_SUPPORTED', `Unknown view: ${String(view)}`);
|
|
154
|
+
}
|
|
155
|
+
function queryAccess(self) {
|
|
156
|
+
const { policy, accessRevision } = readAccessPolicySnapshot(self);
|
|
157
|
+
return ok({
|
|
158
|
+
self,
|
|
159
|
+
view: 'access',
|
|
160
|
+
policy,
|
|
161
|
+
accessRevision,
|
|
162
|
+
});
|
|
128
163
|
}
|
|
129
|
-
function queryContacts(args, self) {
|
|
164
|
+
async function queryContacts(args, self) {
|
|
165
|
+
await expirePendingContactRequests(self);
|
|
130
166
|
const snapshot = getContactSnapshot(self);
|
|
131
167
|
const page = paginate([...snapshot.contacts.keys()].sort(), args, self, primaryId => primaryId);
|
|
132
168
|
const items = page.items.map(primaryId => contactItem(self, primaryId));
|
|
@@ -135,11 +171,13 @@ function queryContacts(args, self) {
|
|
|
135
171
|
view: 'contacts',
|
|
136
172
|
items,
|
|
137
173
|
contactRevision: snapshot.contactRevision,
|
|
174
|
+
pendingCount: [...snapshot.contacts.values()].filter(entry => entry.status === 'pending').length,
|
|
138
175
|
nextCursor: page.nextCursor,
|
|
139
176
|
dataSource: { kind: 'contact-book', authoritative: true, complete: true },
|
|
140
177
|
});
|
|
141
178
|
}
|
|
142
|
-
function queryContact(req, self) {
|
|
179
|
+
async function queryContact(req, self) {
|
|
180
|
+
await expirePendingContactRequests(self);
|
|
143
181
|
const primaryId = String(req.args?.primaryId ?? '').trim();
|
|
144
182
|
if (!primaryId)
|
|
145
183
|
return fail('INVALID_ARGUMENT', 'primaryId is required');
|
|
@@ -150,6 +188,7 @@ function queryContact(req, self) {
|
|
|
150
188
|
}
|
|
151
189
|
function contactItem(self, primaryId) {
|
|
152
190
|
const view = resolveContactView(self, primaryId);
|
|
191
|
+
const pendingRequest = getContactSnapshot(self).contacts.get(primaryId)?.pendingRequest;
|
|
153
192
|
return {
|
|
154
193
|
primaryId,
|
|
155
194
|
displayName: view.displayName ?? null,
|
|
@@ -157,6 +196,13 @@ function contactItem(self, primaryId) {
|
|
|
157
196
|
blocked: view.blocked,
|
|
158
197
|
isOwner: view.isOwner,
|
|
159
198
|
aliases: view.aliases,
|
|
199
|
+
...(pendingRequest ? {
|
|
200
|
+
pendingRequest: {
|
|
201
|
+
id: pendingRequest.id,
|
|
202
|
+
submittedAt: pendingRequest.submittedAt,
|
|
203
|
+
expiresAt: pendingRequest.expiresAt,
|
|
204
|
+
},
|
|
205
|
+
} : {}),
|
|
160
206
|
};
|
|
161
207
|
}
|
|
162
208
|
function normalizeAssignmentItem(item) {
|
|
@@ -181,12 +227,15 @@ function normalizeAssignmentItem(item) {
|
|
|
181
227
|
return item;
|
|
182
228
|
}
|
|
183
229
|
}
|
|
184
|
-
// ── Update
|
|
230
|
+
// ── Update ──
|
|
185
231
|
async function handleUpdate(req, context) {
|
|
186
232
|
const args = req.args ?? {};
|
|
187
|
-
const resource = args.resource;
|
|
233
|
+
const resource = String(args.resource ?? '');
|
|
234
|
+
if (isAccessUpdateResource(resource)) {
|
|
235
|
+
return updateAccessResource(req, context, resource);
|
|
236
|
+
}
|
|
188
237
|
if (resource !== 'assignment' && resource !== 'admin') {
|
|
189
|
-
return fail('
|
|
238
|
+
return fail('NOT_SUPPORTED', `Unknown resource: ${String(resource)}`);
|
|
190
239
|
}
|
|
191
240
|
if (req.value === undefined || !req.value.trim()) {
|
|
192
241
|
return fail('INVALID_ARGUMENT', 'value is required');
|
|
@@ -194,9 +243,9 @@ async function handleUpdate(req, context) {
|
|
|
194
243
|
validateWritableTarget(args.target);
|
|
195
244
|
const value = roleMenuUpdateValue(resource, req.value);
|
|
196
245
|
const data = await roleMenuUpdate(context, args, value);
|
|
197
|
-
return ok(data);
|
|
246
|
+
return ok(resource === 'assignment' ? normalizeAssignmentUpdate(data) : data);
|
|
198
247
|
}
|
|
199
|
-
// ── Action (Contact Book
|
|
248
|
+
// ── Action (Contact Book v3) ──
|
|
200
249
|
async function handleAction(req, self, context) {
|
|
201
250
|
const action = req.action;
|
|
202
251
|
if (action === 'add')
|
|
@@ -205,9 +254,100 @@ async function handleAction(req, self, context) {
|
|
|
205
254
|
return actionSetStatus(req, self, context, true);
|
|
206
255
|
if (action === 'unblock')
|
|
207
256
|
return actionSetStatus(req, self, context, false);
|
|
208
|
-
|
|
257
|
+
if (action === 'approve-request')
|
|
258
|
+
return actionReviewRequest(req, self, context, 'approve');
|
|
259
|
+
if (action === 'reject-request')
|
|
260
|
+
return actionReviewRequest(req, self, context, 'reject');
|
|
261
|
+
if (action === 'block-request')
|
|
262
|
+
return actionReviewRequest(req, self, context, 'block');
|
|
263
|
+
return fail('NOT_SUPPORTED', `Unknown action: ${String(action)}`);
|
|
264
|
+
}
|
|
265
|
+
function isAccessUpdateResource(resource) {
|
|
266
|
+
return resource === 'policyMode'
|
|
267
|
+
|| resource === 'requestLimit';
|
|
268
|
+
}
|
|
269
|
+
function updateAccessResource(req, context, resource) {
|
|
270
|
+
const args = req.args ?? {};
|
|
271
|
+
requireOwnerContext(context);
|
|
272
|
+
if (req.value === undefined || !req.value.trim()) {
|
|
273
|
+
return fail('INVALID_ARGUMENT', 'value is required');
|
|
274
|
+
}
|
|
275
|
+
const self = context.self;
|
|
276
|
+
const expectedAccessRevision = requireAccessRevision(args);
|
|
277
|
+
const decoded = decodeAccessUpdateValue(req.value);
|
|
278
|
+
if (resource === 'policyMode') {
|
|
279
|
+
if (typeof decoded !== 'string' || !['open', 'contacts', 'owners'].includes(decoded)) {
|
|
280
|
+
return fail('INVALID_ARGUMENT', 'policyMode must be open, contacts, or owners');
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
const requestLimit = decoded;
|
|
285
|
+
if (!requestLimit || typeof requestLimit !== 'object' || Array.isArray(requestLimit)
|
|
286
|
+
|| Object.keys(requestLimit).some(key => key !== 'maxTimes' && key !== 'coolDown')
|
|
287
|
+
|| !Number.isInteger(requestLimit.maxTimes)
|
|
288
|
+
|| Number(requestLimit.maxTimes) < 1
|
|
289
|
+
|| Number(requestLimit.maxTimes) > 100_000
|
|
290
|
+
|| !Number.isInteger(requestLimit.coolDown)
|
|
291
|
+
|| Number(requestLimit.coolDown) < 1
|
|
292
|
+
|| Number(requestLimit.coolDown) > 86_400) {
|
|
293
|
+
return fail('INVALID_ARGUMENT', 'requestLimit must be a JSON object with integer maxTimes and coolDown');
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const result = mutateAccessPolicy({
|
|
297
|
+
selfAid: self,
|
|
298
|
+
actorAid: String(context.actorAid || ''),
|
|
299
|
+
actorRole: context.actorRole,
|
|
300
|
+
resource: `access.${resource}`,
|
|
301
|
+
expectedAccessRevision,
|
|
302
|
+
update: current => accessPolicyWithUpdate(current, resource, decoded),
|
|
303
|
+
});
|
|
304
|
+
return ok({
|
|
305
|
+
self,
|
|
306
|
+
resource,
|
|
307
|
+
policy: result.policy,
|
|
308
|
+
accessRevision: result.accessRevision,
|
|
309
|
+
changed: result.changed,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
async function actionReviewRequest(req, self, context, decision) {
|
|
313
|
+
const args = req.args ?? {};
|
|
314
|
+
const primaryId = String(args.primaryId ?? '').trim();
|
|
315
|
+
const requestId = String(args.requestId ?? '').trim();
|
|
316
|
+
if (!primaryId)
|
|
317
|
+
return fail('INVALID_ARGUMENT', 'primaryId is required');
|
|
318
|
+
if (!requestId)
|
|
319
|
+
return fail('INVALID_ARGUMENT', 'requestId is required');
|
|
320
|
+
requireAdminContext(context);
|
|
321
|
+
const expectedContactRevision = requireContactRevision(args);
|
|
322
|
+
const actorId = String(context.actorAid || '').trim();
|
|
323
|
+
const reviewed = await reviewContactRequest({
|
|
324
|
+
selfAid: self,
|
|
325
|
+
primaryId,
|
|
326
|
+
requestId,
|
|
327
|
+
expectedContactRevision,
|
|
328
|
+
approverId: actorId,
|
|
329
|
+
actorId,
|
|
330
|
+
reviewerRole: context.actorRole,
|
|
331
|
+
decision,
|
|
332
|
+
});
|
|
333
|
+
if (reviewed.code === 'approved' || reviewed.code === 'rejected' || reviewed.code === 'blocked') {
|
|
334
|
+
return ok({
|
|
335
|
+
self,
|
|
336
|
+
action: `${decision}-request`,
|
|
337
|
+
primaryId,
|
|
338
|
+
requestId,
|
|
339
|
+
result: reviewed.code,
|
|
340
|
+
contactRevision: reviewed.contactRevision,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
const code = reviewed.code === 'forbidden' ? 'PERMISSION_DENIED'
|
|
344
|
+
: reviewed.code === 'revision-conflict' ? 'CONFLICT'
|
|
345
|
+
: reviewed.code === 'request-expired' ? 'EXPIRED'
|
|
346
|
+
: 'NOT_FOUND';
|
|
347
|
+
return fail(code, reviewed.code.replace(/-/g, ' '), { contactRevision: reviewed.contactRevision });
|
|
209
348
|
}
|
|
210
349
|
async function actionAdd(req, self, context) {
|
|
350
|
+
requireAdminContext(context);
|
|
211
351
|
const args = req.args ?? {};
|
|
212
352
|
const primaryId = String(args.primaryId ?? '').trim();
|
|
213
353
|
if (!primaryId)
|
|
@@ -219,11 +359,13 @@ async function actionAdd(req, self, context) {
|
|
|
219
359
|
}
|
|
220
360
|
const current = getContactSnapshot(self).contacts.get(primaryId);
|
|
221
361
|
const displayName = hasDisplayName ? args.displayName : current?.displayName ?? null;
|
|
222
|
-
const result = await
|
|
362
|
+
const result = await mutateContactWithOperation({
|
|
223
363
|
selfAid: self,
|
|
224
|
-
|
|
364
|
+
actorId: context.actorAid ?? 'connect-menu',
|
|
365
|
+
operation: 'manual-add',
|
|
225
366
|
expectedContactRevision,
|
|
226
367
|
mutation: { type: 'set-display-name', primaryId, displayName },
|
|
368
|
+
primaryId,
|
|
227
369
|
});
|
|
228
370
|
const storedDisplayName = result.contact.contacts[primaryId]?.displayName ?? null;
|
|
229
371
|
return ok({
|
|
@@ -236,16 +378,22 @@ async function actionAdd(req, self, context) {
|
|
|
236
378
|
});
|
|
237
379
|
}
|
|
238
380
|
async function actionSetStatus(req, self, context, blocked) {
|
|
381
|
+
requireAdminContext(context);
|
|
239
382
|
const args = req.args ?? {};
|
|
240
383
|
const primaryId = String(args.primaryId ?? '').trim();
|
|
241
384
|
if (!primaryId)
|
|
242
385
|
return fail('INVALID_ARGUMENT', 'primaryId is required');
|
|
243
386
|
const expectedContactRevision = requireContactRevision(args);
|
|
244
|
-
|
|
387
|
+
if (!getContactSnapshot(self).contacts.has(primaryId)) {
|
|
388
|
+
return fail('NOT_FOUND', `Contact was not found: ${primaryId}`);
|
|
389
|
+
}
|
|
390
|
+
const result = await mutateContactWithOperation({
|
|
245
391
|
selfAid: self,
|
|
246
|
-
|
|
392
|
+
actorId: context.actorAid ?? 'connect-menu',
|
|
393
|
+
operation: blocked ? 'block' : 'unblock',
|
|
247
394
|
expectedContactRevision,
|
|
248
395
|
mutation: { type: 'set-status', primaryId, status: blocked ? 'blocked' : 'active' },
|
|
396
|
+
primaryId,
|
|
249
397
|
});
|
|
250
398
|
return ok({
|
|
251
399
|
self,
|
|
@@ -259,6 +407,16 @@ async function actionSetStatus(req, self, context, blocked) {
|
|
|
259
407
|
function ok(data) {
|
|
260
408
|
return { type: 'menu.response', id: '', name: MENU_NAME, data };
|
|
261
409
|
}
|
|
410
|
+
function requireOwnerContext(context) {
|
|
411
|
+
if (context.actorRole !== 'owner') {
|
|
412
|
+
throw connectError('PERMISSION_DENIED', 'Only an Agent Owner can modify this access setting');
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
function requireAdminContext(context) {
|
|
416
|
+
if (context.actorRole !== 'owner' && context.actorRole !== 'admin') {
|
|
417
|
+
throw connectError('PERMISSION_DENIED', 'Only an Agent Owner or Admin can perform this operation');
|
|
418
|
+
}
|
|
419
|
+
}
|
|
262
420
|
function fail(code, message, data) {
|
|
263
421
|
return {
|
|
264
422
|
type: 'menu.response',
|
|
@@ -277,6 +435,16 @@ function errorCode(error) {
|
|
|
277
435
|
return 'CONFLICT';
|
|
278
436
|
if (code === 'INVALID_CONTACT_BOOK')
|
|
279
437
|
return 'INTERNAL_ERROR';
|
|
438
|
+
if (code === 'access_policy_owner_required')
|
|
439
|
+
return 'PERMISSION_DENIED';
|
|
440
|
+
if (code === 'access_policy_revision_conflict')
|
|
441
|
+
return 'CONFLICT';
|
|
442
|
+
if (code === 'access_policy_temporarily_unavailable')
|
|
443
|
+
return 'TEMPORARILY_UNAVAILABLE';
|
|
444
|
+
if (code === 'restricted_mode_without_owner')
|
|
445
|
+
return 'NOT_ALLOWED';
|
|
446
|
+
if (code === 'invalid_access_policy')
|
|
447
|
+
return 'INTERNAL_ERROR';
|
|
280
448
|
return typeof code === 'string' ? code : 'INTERNAL_ERROR';
|
|
281
449
|
}
|
|
282
450
|
function errorMessage(error) {
|
|
@@ -297,6 +465,15 @@ function requireContactRevision(args) {
|
|
|
297
465
|
}
|
|
298
466
|
return args.expectedContactRevision;
|
|
299
467
|
}
|
|
468
|
+
function requireAccessRevision(args) {
|
|
469
|
+
if (!Object.prototype.hasOwnProperty.call(args, 'expectedAccessRevision')) {
|
|
470
|
+
throw connectError('INVALID_ARGUMENT', 'expectedAccessRevision is required');
|
|
471
|
+
}
|
|
472
|
+
if (typeof args.expectedAccessRevision !== 'string' || !args.expectedAccessRevision) {
|
|
473
|
+
throw connectError('INVALID_ARGUMENT', 'expectedAccessRevision must be a non-empty string');
|
|
474
|
+
}
|
|
475
|
+
return args.expectedAccessRevision;
|
|
476
|
+
}
|
|
300
477
|
function decodeUpdateValue(value) {
|
|
301
478
|
const text = String(value ?? '').trim();
|
|
302
479
|
if (!text)
|
|
@@ -309,6 +486,15 @@ function decodeUpdateValue(value) {
|
|
|
309
486
|
catch { }
|
|
310
487
|
return text;
|
|
311
488
|
}
|
|
489
|
+
function decodeAccessUpdateValue(value) {
|
|
490
|
+
const text = value.trim();
|
|
491
|
+
try {
|
|
492
|
+
return JSON.parse(text);
|
|
493
|
+
}
|
|
494
|
+
catch {
|
|
495
|
+
return text;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
312
498
|
function roleMenuUpdateValue(resource, value) {
|
|
313
499
|
const decoded = decodeUpdateValue(value);
|
|
314
500
|
if (resource === 'assignment') {
|
|
@@ -317,11 +503,28 @@ function roleMenuUpdateValue(resource, value) {
|
|
|
317
503
|
}
|
|
318
504
|
return JSON.stringify(decoded);
|
|
319
505
|
}
|
|
320
|
-
if (
|
|
506
|
+
if (decoded !== 'grant' && decoded !== 'revoke') {
|
|
321
507
|
throw connectError('INVALID_ARGUMENT', 'admin value must be grant or revoke');
|
|
322
508
|
}
|
|
323
509
|
return JSON.stringify(decoded);
|
|
324
510
|
}
|
|
511
|
+
function normalizeAssignmentUpdate(data) {
|
|
512
|
+
const explicitRole = data?.assignment?.explicitRole;
|
|
513
|
+
return {
|
|
514
|
+
...data,
|
|
515
|
+
roleId: typeof explicitRole === 'string' ? explicitRole : null,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
function accessPolicyWithUpdate(current, resource, decoded) {
|
|
519
|
+
if (resource === 'policyMode') {
|
|
520
|
+
return { ...current, policyMode: decoded };
|
|
521
|
+
}
|
|
522
|
+
const requestLimit = decoded;
|
|
523
|
+
return {
|
|
524
|
+
...current,
|
|
525
|
+
requestLimit: { maxTimes: requestLimit.maxTimes, coolDown: requestLimit.coolDown },
|
|
526
|
+
};
|
|
527
|
+
}
|
|
325
528
|
function validateWritableTarget(target) {
|
|
326
529
|
if (!target || typeof target !== 'object' || Array.isArray(target))
|
|
327
530
|
return;
|