evolcore 0.0.10 → 0.0.12
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 -0
- package/README.md +3 -3
- package/bin/codex-managed-hook.mjs +80 -0
- package/dist/agents/baseagent.js +4 -0
- package/dist/agents/claude-runner.js +158 -44
- package/dist/agents/codex-app-server-client.js +153 -23
- package/dist/agents/codex-runner.js +331 -43
- package/dist/agents/ecagent-runner.js +53 -6
- package/dist/agents/gemini-runner.js +1 -0
- package/dist/agents/request-identity.js +55 -0
- package/dist/aun/aid/client.js +11 -21
- package/dist/aun/aid/managed-operation.js +107 -0
- package/dist/aun/msg/group.js +81 -12
- package/dist/aun/msg/managed-operation.js +804 -0
- package/dist/aun/msg/mention-schema.js +20 -0
- package/dist/aun/msg/p2p.js +7 -0
- package/dist/aun/outbox.js +28 -31
- package/dist/channels/aun.js +418 -235
- package/dist/channels/daemon.js +13 -0
- package/dist/cli/agent-command.js +72 -17
- package/dist/cli/agent.js +249 -22
- package/dist/cli/aun-commands.js +270 -69
- package/dist/cli/bench.js +12 -3
- package/dist/cli/daemon-commands.js +52 -33
- package/dist/cli/fs-command.js +60 -0
- package/dist/cli/handoff-command.js +3 -0
- package/dist/cli/index.js +106 -63
- package/dist/cli/init-cancel.js +208 -0
- package/dist/cli/init-channel.js +343 -195
- package/dist/cli/init.js +21 -9
- package/dist/cli/managed-command-guard.js +41 -0
- package/dist/cli/model.js +11 -4
- package/dist/cli/queue-command.js +11 -1
- package/dist/cli/response.js +71 -0
- package/dist/cli/restart-monitor.js +4 -18
- package/dist/cli/trigger-command.js +4 -0
- package/dist/config/builtin-roles.js +5 -0
- package/dist/config/contact-book-store.js +11 -3
- package/dist/config/contact-request-service.js +161 -15
- package/dist/config/gateway-config.js +26 -10
- package/dist/config/mention-mode.js +8 -24
- package/dist/core/agent-reload-coordinator.js +53 -0
- package/dist/core/auth/agent-delegation.js +0 -1
- package/dist/core/auth/operation-authorizer.js +62 -154
- package/dist/core/auth/operation-catalog.js +264 -22
- package/dist/core/bootstrap-messages.js +68 -0
- package/dist/core/bootstrap-service.js +144 -10
- package/dist/core/capability/providers/claude-capability-provider.js +9 -31
- package/dist/core/capability/providers/codex-capability-provider.js +4 -15
- package/dist/core/capability/skill-discovery.js +55 -0
- package/dist/core/channel-loader.js +24 -7
- package/dist/core/command/agent-control.js +14 -11
- package/dist/core/command/cli-intent-parser.js +9 -0
- package/dist/core/command/command-handler.js +467 -6
- package/dist/core/command/connect-menu.js +15 -0
- package/dist/core/command/group-menu.js +4 -4
- package/dist/core/command/menu-catalog.js +366 -0
- package/dist/core/command/menu-handler.js +213 -110
- package/dist/core/command/menu-protocol.js +71 -0
- package/dist/core/command/slash-gate.js +5 -2
- package/dist/core/command/slash-handler.js +45 -25
- package/dist/core/daemon-file-cache.js +2 -1
- package/dist/core/data-migration.js +9 -2
- package/dist/core/evolagent-registry.js +125 -35
- package/dist/core/evolagent.js +12 -5
- package/dist/core/handoff/dispatcher.js +5 -2
- package/dist/core/handoff/runtime.js +145 -21
- package/dist/core/handoff/store.js +24 -0
- package/dist/core/handoff/types.js +1 -0
- package/dist/core/inference/text-inference.js +38 -4
- package/dist/core/message/logical-queue-bridge.js +1 -0
- package/dist/core/message/mention-schema.js +126 -0
- package/dist/core/message/message-bridge.js +237 -79
- package/dist/core/message/message-log.js +23 -0
- package/dist/core/message/message-queue.js +20 -4
- package/dist/core/message/response-engine.js +54 -36
- package/dist/core/model/model-catalog.js +143 -24
- package/dist/core/model/model-diagnostics.js +28 -10
- package/dist/core/permission/ec-command-parser.js +156 -10
- package/dist/core/permission/index.js +1 -0
- package/dist/core/permission/readonly-shell-query.js +532 -0
- package/dist/core/permission/sandbox-runtime.js +22 -1
- package/dist/core/permission/shell-environment.js +46 -0
- package/dist/core/permission/tool-policy.js +292 -111
- package/dist/core/protected-paths.js +17 -9
- package/dist/core/runner-reload-transaction.js +57 -0
- package/dist/core/session/session-manager.js +21 -2
- package/dist/eck/manifest-engine.js +39 -13
- package/dist/index.js +644 -180
- package/dist/ipc.js +246 -19
- package/dist/response-system/coordinator.js +6 -4
- package/dist/response-system/engines/v1/proactive-flow.js +26 -8
- package/dist/response-system/modes/single-session/index.js +14 -1
- package/dist/response-system/selector.js +6 -5
- package/dist/trigger/anomaly-store.js +4 -0
- package/dist/trigger/script-executor.js +1 -0
- package/dist/utils/aid-bind.js +3 -8
- package/dist/utils/codex-app-server-registry.js +90 -0
- package/dist/utils/codex-cli.js +5 -1
- package/dist/utils/cross-platform.js +15 -0
- package/dist/utils/log-writer.js +6 -10
- package/dist/utils/logger.js +5 -5
- package/dist/utils/npm-ops.js +5 -12
- package/dist/utils/tool-summary.js +11 -0
- package/kits/docs/channels/aun.md +1 -1
- package/kits/docs/context-assembly.md +2 -2
- package/kits/docs/evolcore/agent.md +11 -6
- package/kits/docs/evolcore/aid.md +14 -0
- package/kits/docs/evolcore/msg.md +13 -0
- package/kits/docs/prompt-loading-architecture.md +1 -2
- package/kits/docs/venues/group.md +1 -1
- package/kits/eck_manifest.json +0 -12
- package/kits/rules/01-overview.md +9 -0
- package/kits/rules/04-relation.md +1 -1
- package/kits/rules/05-venue.md +2 -2
- package/kits/schemas/agent-config.schema.3.json +1 -1
- package/kits/schemas/agent-config.schema.4.json +1 -1
- package/kits/schemas/relation-config.schema.2.json +1 -1
- package/kits/schemas/role-config.schema.1.json +1 -1
- package/kits/schemas/single-session.schema.1.json +3 -3
- package/kits/schemas/single-session.schema.2.json +3 -3
- package/kits/templates/roles/admin.json +27 -0
- package/kits/templates/roles/member.json +44 -0
- package/kits/templates/roles/visitor.json +40 -2
- package/kits/templates/system-fragments/bootstrap.md +12 -6
- package/kits/templates/system-fragments/channel.md +6 -0
- package/kits/templates/system-fragments/session.md +2 -0
- package/package.json +3 -2
- package/skills/eclink/SKILL.md +15 -3
- package/skills/eclink/agents/openai.yaml +3 -3
- package/dist/core/command/evol-menu-version-gate.js +0 -39
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { groupInfo, groupMembers } from '../../aun/msg/group.js';
|
|
3
|
+
import { formatPeerKey } from '../relation/peer-identity.js';
|
|
4
|
+
import { authorizeAccess, authorizeOperation, buildAuthSubject } from '../auth/auth-gateway.js';
|
|
5
|
+
import { ConfigTarget, read as readConfig } from '../../config/config-manager.js';
|
|
6
|
+
import { readRolesConfig } from '../../config/roles.js';
|
|
7
|
+
import { normalizeMenuError } from './menu-protocol.js';
|
|
8
|
+
import { isValidAid } from '../../aun/aid/validation.js';
|
|
9
|
+
const DEFAULT_DIRECTORY = { getGroup: groupInfo, getMembers: groupMembers };
|
|
10
|
+
export function validateMenuValueBatchArgs(args) {
|
|
11
|
+
if (!args || typeof args !== 'object' || Array.isArray(args))
|
|
12
|
+
throw { code: 'INVALID_ARGUMENT', message: 'batch args must be an object' };
|
|
13
|
+
const allowed = new Set(['view', 'schemaVersion', 'consistency', 'context', 'items']);
|
|
14
|
+
const unknown = Object.keys(args).find(key => !allowed.has(key));
|
|
15
|
+
if (unknown)
|
|
16
|
+
throw { code: 'INVALID_ARGUMENT', message: `unsupported batch argument: ${unknown}` };
|
|
17
|
+
if (args.view !== 'values')
|
|
18
|
+
throw { code: 'NOT_SUPPORTED', message: 'batch requires args.view=values' };
|
|
19
|
+
}
|
|
20
|
+
export function validateMenuCatalogArgs(args) {
|
|
21
|
+
if (!args || typeof args !== 'object' || Array.isArray(args))
|
|
22
|
+
throw { code: 'INVALID_ARGUMENT', message: 'catalog args must be an object' };
|
|
23
|
+
const allowed = new Set(['view', 'acceptSchemaVersions', 'context']);
|
|
24
|
+
const unknown = Object.keys(args).find(key => !allowed.has(key));
|
|
25
|
+
if (unknown)
|
|
26
|
+
throw { code: 'INVALID_ARGUMENT', message: `unsupported catalog argument: ${unknown}` };
|
|
27
|
+
if (args.view !== 'catalog')
|
|
28
|
+
throw { code: 'NOT_SUPPORTED', message: 'catalog requires args.view=catalog' };
|
|
29
|
+
assertMenuSchemaVersion(args.acceptSchemaVersions);
|
|
30
|
+
}
|
|
31
|
+
function normalizeContextRequest(value) {
|
|
32
|
+
if (value === undefined || value === null)
|
|
33
|
+
return undefined;
|
|
34
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
35
|
+
throw { code: 'INVALID_CONTEXT', message: 'context must be an object' };
|
|
36
|
+
}
|
|
37
|
+
const raw = value;
|
|
38
|
+
const unknown = Object.keys(raw).find(key => key !== 'kind' && key !== 'groupId');
|
|
39
|
+
if (unknown)
|
|
40
|
+
throw { code: 'INVALID_CONTEXT', message: `context field is unsupported: ${unknown}` };
|
|
41
|
+
if (raw.kind !== undefined && (typeof raw.kind !== 'string' || !['private_relation', 'group_relation', 'agent', 'process'].includes(raw.kind))) {
|
|
42
|
+
throw { code: 'INVALID_CONTEXT', message: 'context.kind is unsupported' };
|
|
43
|
+
}
|
|
44
|
+
if (raw.groupId !== undefined && (typeof raw.groupId !== 'string' || !raw.groupId.trim())) {
|
|
45
|
+
throw { code: 'INVALID_CONTEXT', message: 'context.groupId must be a non-empty string' };
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
...(raw.kind !== undefined ? { kind: raw.kind } : {}),
|
|
49
|
+
...(raw.groupId !== undefined ? { groupId: raw.groupId.trim() } : {}),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export function assertMenuSchemaVersion(value) {
|
|
53
|
+
if (value === undefined || value === null)
|
|
54
|
+
return;
|
|
55
|
+
const versions = Array.isArray(value) ? value : [value];
|
|
56
|
+
if (!versions.some(version => version === 1)) {
|
|
57
|
+
throw { code: 'SCHEMA_VERSION_UNSUPPORTED', message: 'Only Menu schema version 1 is supported' };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const relation = (id, name, label, order, cmd, operation, extra) => ({
|
|
61
|
+
id, type: 'setting', contexts: ['private_relation', 'group_relation'], name, label, section: 'relation_behavior', order,
|
|
62
|
+
query: { cmd, operation, scope: 'relation' }, options: { cmd, operation: operation === 'model.current' ? 'model.list' : operation, scope: 'relation' },
|
|
63
|
+
...extra,
|
|
64
|
+
});
|
|
65
|
+
const ENTRIES = [
|
|
66
|
+
relation('model.relation', 'model', '模型', 10, '/model', 'model.current', { options: { cmd: '/model', operation: 'model.list', scope: 'relation', mode: 'dynamic' }, update: { cmd: '/model', operation: 'model.use', scope: 'relation' }, control: 'select' }),
|
|
67
|
+
relation('effort.relation', 'effort', '推理强度', 20, '/effort', 'model.effort.current', { options: { cmd: '/effort', operation: 'model.effort.current', scope: 'relation', mode: 'dynamic', dependsOn: ['model.relation'] }, update: { cmd: '/effort', operation: 'model.effort', scope: 'relation' }, control: 'select' }),
|
|
68
|
+
relation('chatmode.relation', 'chatmode', '对话模式', 30, '/chatmode', 'chatmode.current', { options: { cmd: '/chatmode', operation: 'chatmode.current', scope: 'relation', mode: 'static', items: [{ value: 'interactive', label: '交互模式' }, { value: 'proactive', label: '主动模式' }] }, update: { cmd: '/chatmode', operation: 'chatmode.update', scope: 'relation' }, control: 'select' }),
|
|
69
|
+
relation('activity.relation', 'activity', '中间输出', 40, '/activity', 'activity.current', { options: { cmd: '/activity', operation: 'activity.current', scope: 'relation', mode: 'static', items: [{ value: 'all', label: '私聊显示' }, { value: 'text', label: '仅文字进展' }, { value: 'none', label: '全部静默' }] }, update: { cmd: '/activity', operation: 'activity.update', scope: 'relation' }, control: 'select' }),
|
|
70
|
+
relation('permissionMode.relation', 'permission', '权限模式', 50, '/perm', 'permission.current', { query: { cmd: '/perm', args: { scope: 'role' }, operation: 'permission.current', scope: 'relation' }, options: undefined, update: { cmd: '/perm', operation: 'permission.current', scope: 'relation', supported: false }, control: 'select' }),
|
|
71
|
+
relation('mentionMode.relation', 'group', '群消息响应', 60, '/group', 'group.mentionmode.current', { contexts: ['group_relation'], query: { cmd: '/group', args: { view: 'mentionMode', scope: 'relation' }, operation: 'group.mentionmode.current', scope: 'relation' }, options: { cmd: '/group', args: { option: 'mentionMode', scope: 'relation' }, operation: 'group.mentionmode.current', scope: 'relation' }, update: { cmd: '/group', args: { resource: 'mentionMode', scope: 'relation' }, operation: 'group.mentionmode.update', scope: 'relation' }, control: 'select' }),
|
|
72
|
+
relation('rulesPolicy.relation', 'group', '群规则策略', 70, '/group', 'group.rulespolicy.current', { contexts: ['group_relation'], query: { cmd: '/group', args: { view: 'rulesPolicy', scope: 'relation' }, operation: 'group.rulespolicy.current', scope: 'relation' }, options: { cmd: '/group', args: { option: 'rulesPolicy', scope: 'relation' }, operation: 'group.rulespolicy.current', scope: 'relation' }, update: { cmd: '/group', args: { resource: 'rulesPolicy', scope: 'relation' }, operation: 'group.rulespolicy.update', scope: 'relation' }, control: 'select' }),
|
|
73
|
+
{ id: 'baseagent.agent', type: 'setting', contexts: ['agent'], name: 'baseagent', label: '运行后端', section: 'agent', order: 10, query: { cmd: '/baseagent', args: { scope: 'agent' }, operation: 'agent.baseagent.current', scope: 'agent' }, options: { cmd: '/baseagent', args: { scope: 'agent' }, operation: 'agent.baseagent.list', scope: 'agent', mode: 'dynamic' }, update: { cmd: '/baseagent', args: { scope: 'agent' }, operation: 'agent.baseagent.update', scope: 'agent' }, valueSchema: { type: 'string', nullable: true }, control: 'select' },
|
|
74
|
+
{ id: 'model.agent', type: 'setting', contexts: ['agent'], name: 'model', label: 'Agent 默认模型', section: 'agent', order: 20, query: { cmd: '/model', args: { scope: 'agent' }, operation: 'model.current', scope: 'agent' }, options: { cmd: '/model', args: { scope: 'agent' }, operation: 'model.list', scope: 'agent' }, update: { cmd: '/model', args: { scope: 'agent' }, operation: 'model.use', scope: 'agent' }, control: 'select' },
|
|
75
|
+
{ id: 'effort.agent', type: 'setting', contexts: ['agent'], name: 'effort', label: 'Agent 默认强度', section: 'agent', order: 30, query: { cmd: '/effort', args: { scope: 'agent' }, operation: 'model.effort.current', scope: 'agent' }, options: { cmd: '/effort', args: { scope: 'agent' }, operation: 'model.effort.current', scope: 'agent', mode: 'dynamic', dependsOn: ['baseagent.agent', 'model.agent'] }, update: { cmd: '/effort', args: { scope: 'agent' }, operation: 'model.effort', scope: 'agent' }, control: 'select' },
|
|
76
|
+
{ id: 'chatmode.agent.private', type: 'setting', contexts: ['agent'], name: 'chatmode', label: 'Agent 私聊模式', section: 'agent', order: 41, query: { cmd: '/chatmode', args: { scope: 'agent', field: 'private' }, operation: 'chatmode.current', scope: 'agent' }, options: { cmd: '/chatmode', args: { scope: 'agent', field: 'private' }, operation: 'chatmode.current', scope: 'agent' }, update: { cmd: '/chatmode', args: { scope: 'agent', field: 'private' }, operation: 'chatmode.update', scope: 'agent' }, control: 'select' },
|
|
77
|
+
{ id: 'chatmode.agent.group', type: 'setting', contexts: ['agent'], name: 'chatmode', label: 'Agent 群聊模式', section: 'agent', order: 42, query: { cmd: '/chatmode', args: { scope: 'agent', field: 'group' }, operation: 'chatmode.current', scope: 'agent' }, options: { cmd: '/chatmode', args: { scope: 'agent', field: 'group' }, operation: 'chatmode.current', scope: 'agent' }, update: { cmd: '/chatmode', args: { scope: 'agent', field: 'group' }, operation: 'chatmode.update', scope: 'agent' }, control: 'select' },
|
|
78
|
+
{ id: 'chatmode.agent.nothuman', type: 'setting', contexts: ['agent'], name: 'chatmode', label: 'Agent 非人类对话模式', section: 'agent', order: 43, query: { cmd: '/chatmode', args: { scope: 'agent', field: 'nothuman' }, operation: 'chatmode.current', scope: 'agent' }, options: { cmd: '/chatmode', args: { scope: 'agent', field: 'nothuman' }, operation: 'chatmode.current', scope: 'agent' }, update: { cmd: '/chatmode', args: { scope: 'agent', field: 'nothuman' }, operation: 'chatmode.update', scope: 'agent' }, control: 'select' },
|
|
79
|
+
{ id: 'activity.agent', type: 'setting', contexts: ['agent'], name: 'activity', label: 'Agent 中间输出', section: 'agent', order: 50, query: { cmd: '/activity', args: { scope: 'agent' }, operation: 'activity.current', scope: 'agent' }, options: { cmd: '/activity', args: { scope: 'agent' }, operation: 'activity.current', scope: 'agent' }, update: { cmd: '/activity', args: { scope: 'agent' }, operation: 'activity.update', scope: 'agent' }, control: 'select' },
|
|
80
|
+
{ id: 'observable.agent', type: 'setting', contexts: ['agent'], name: 'observable', label: '观察模式', section: 'agent', order: 60, query: { cmd: '/observable', operation: 'observable.current', scope: 'agent' }, update: { cmd: '/observable', operation: 'observable.update', scope: 'agent' }, valueSchema: { type: 'boolean' }, control: 'toggle' },
|
|
81
|
+
{ id: 'system.status', type: 'list', contexts: ['process'], name: 'system', label: '系统状态', section: 'process', order: 10, query: { cmd: '/system', operation: 'system.status', scope: 'process' }, control: 'list' },
|
|
82
|
+
{ id: 'system.restart', type: 'action', contexts: ['process'], name: 'system', label: '重启服务', section: 'process', order: 20, action: { cmd: '/system', action: 'restart', operation: 'system.restart', scope: 'process' } },
|
|
83
|
+
{ id: 'system.upgrade', type: 'action', contexts: ['process'], name: 'system', label: '升级服务', section: 'process', order: 30, action: { cmd: '/system', action: 'upgrade', operation: 'system.upgrade', scope: 'process' } },
|
|
84
|
+
{ id: 'agent.list', type: 'list', contexts: ['process'], name: 'agent', label: 'Agent 列表', section: 'process', order: 40, options: { cmd: '/agent', args: { options: 'all' }, operation: 'agent.list', scope: 'control', mode: 'dynamic' }, control: 'list' },
|
|
85
|
+
{ id: 'agent.create', type: 'action', contexts: ['process'], name: 'agent', label: '创建 Agent', section: 'process', order: 50, action: { cmd: '/agent', action: 'create', operation: 'agent.create', scope: 'control' } },
|
|
86
|
+
{ id: 'agent.reload', type: 'action', contexts: ['process'], name: 'agent', label: '重载 Agent', section: 'process', order: 60, action: { cmd: '/agent', action: 'reload', operation: 'agent.reload', scope: 'control' } },
|
|
87
|
+
{ id: 'agent.delete', type: 'action', contexts: ['process'], name: 'agent', label: '删除 Agent', section: 'process', order: 70, action: { cmd: '/agent', action: 'delete', operation: 'agent.delete', scope: 'control' } },
|
|
88
|
+
];
|
|
89
|
+
function stable(value) {
|
|
90
|
+
if (Array.isArray(value))
|
|
91
|
+
return `[${value.map(stable).join(',')}]`;
|
|
92
|
+
if (!value || typeof value !== 'object')
|
|
93
|
+
return JSON.stringify(value);
|
|
94
|
+
return `{${Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${JSON.stringify(k)}:${stable(v)}`).join(',')}}`;
|
|
95
|
+
}
|
|
96
|
+
function hash(value) { return `sha256:${createHash('sha256').update(stable(value)).digest('hex')}`; }
|
|
97
|
+
export async function resolveMenuCatalogContext(input) {
|
|
98
|
+
const { subject } = input;
|
|
99
|
+
const request = normalizeContextRequest(input.request);
|
|
100
|
+
const requested = request?.kind ?? (subject.chatType === 'group' ? 'group_relation' : 'private_relation');
|
|
101
|
+
const transport = String(input.channelType || subject.channelType || 'unknown');
|
|
102
|
+
if (request?.groupId && requested !== 'group_relation') {
|
|
103
|
+
throw { code: 'INVALID_CONTEXT', message: 'groupId is only valid for group_relation context' };
|
|
104
|
+
}
|
|
105
|
+
if (input.channelScope === 'control' && !subject.fromControlChannel) {
|
|
106
|
+
throw { code: 'FORBIDDEN', message: 'control catalog context requires a control subject' };
|
|
107
|
+
}
|
|
108
|
+
if (requested === 'process') {
|
|
109
|
+
if (input.channelScope !== 'control' || !subject.fromControlChannel || !subject.isDaemonOwner)
|
|
110
|
+
throw { code: 'FORBIDDEN', message: 'process context requires a daemon owner control subject' };
|
|
111
|
+
return makeContext(input, requested, subject, input.channelId);
|
|
112
|
+
}
|
|
113
|
+
if (requested === 'agent') {
|
|
114
|
+
if (!subject.selfAid) {
|
|
115
|
+
throw { code: 'NOT_FOUND', message: 'agent context requires a receiving Agent' };
|
|
116
|
+
}
|
|
117
|
+
const bound = {
|
|
118
|
+
...subject,
|
|
119
|
+
chatType: 'private',
|
|
120
|
+
conversationId: subject.selfAid,
|
|
121
|
+
channelId: subject.selfAid,
|
|
122
|
+
peerKey: undefined,
|
|
123
|
+
};
|
|
124
|
+
return makeContext({ ...input, channelId: bound.channelId }, requested, bound, bound.channelId);
|
|
125
|
+
}
|
|
126
|
+
if (requested === 'private_relation') {
|
|
127
|
+
if (subject.chatType === 'group')
|
|
128
|
+
throw { code: 'INVALID_CONTEXT', message: 'group transport cannot request private_relation' };
|
|
129
|
+
if (transport !== 'aun' || !subject.selfAid || !subject.actorId || !isValidAid(subject.actorId))
|
|
130
|
+
throw { code: 'INVALID_CONTEXT', message: 'private_relation requires an authenticated AUN actor and Agent' };
|
|
131
|
+
const rebuilt = buildAuthSubject({
|
|
132
|
+
selfAid: subject.selfAid,
|
|
133
|
+
actorId: subject.actorId,
|
|
134
|
+
channel: subject.channel,
|
|
135
|
+
channelType: transport,
|
|
136
|
+
channelId: subject.actorId,
|
|
137
|
+
chatType: 'private',
|
|
138
|
+
conversationId: subject.actorId,
|
|
139
|
+
fromControlChannel: subject.fromControlChannel,
|
|
140
|
+
identity: subject.isDaemonOwner ? subject.identity : undefined,
|
|
141
|
+
});
|
|
142
|
+
const bound = { ...rebuilt, channel: subject.channel, channelId: subject.actorId, peerKey: formatPeerKey('aun', subject.actorId), isDaemonOwner: subject.isDaemonOwner || rebuilt.isDaemonOwner, fromControlChannel: subject.fromControlChannel };
|
|
143
|
+
const access = authorizeAccess(bound);
|
|
144
|
+
if (!access.allow)
|
|
145
|
+
throw { code: access.code, message: access.reason };
|
|
146
|
+
return makeContext({ ...input, channelId: subject.actorId }, requested, bound, subject.actorId);
|
|
147
|
+
}
|
|
148
|
+
if (transport !== 'aun' || !subject.selfAid || !subject.actorId || !isValidAid(subject.actorId))
|
|
149
|
+
throw { code: 'INVALID_CONTEXT', message: 'group_relation requires an authenticated AUN actor and Agent' };
|
|
150
|
+
const groupId = String(request?.groupId || subject.conversationId || '').trim();
|
|
151
|
+
if (!groupId)
|
|
152
|
+
throw { code: 'INVALID_CONTEXT', message: 'group_relation requires groupId' };
|
|
153
|
+
const directory = input.directory ?? DEFAULT_DIRECTORY;
|
|
154
|
+
let info;
|
|
155
|
+
try {
|
|
156
|
+
info = await directory.getGroup({ from: subject.selfAid, groupId, required: ['member', 'state'] });
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
throw { code: 'TEMPORARILY_UNAVAILABLE', message: 'unable to verify target group' };
|
|
160
|
+
}
|
|
161
|
+
if (!info.ok || !info.found || !info.group || !info.group.my_role || info.group.status === 'dissolved')
|
|
162
|
+
throw { code: info.ok ? 'NOT_FOUND' : 'TEMPORARILY_UNAVAILABLE', message: 'target group is not joined by this Agent' };
|
|
163
|
+
const canonicalGroupId = String(info.group.group_aid || info.group.group_id || groupId);
|
|
164
|
+
const pageSize = 200;
|
|
165
|
+
let page = 1;
|
|
166
|
+
let total = 0;
|
|
167
|
+
let actor;
|
|
168
|
+
do {
|
|
169
|
+
let members;
|
|
170
|
+
try {
|
|
171
|
+
members = await directory.getMembers({ from: subject.selfAid, groupId: canonicalGroupId, page, size: pageSize });
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
throw { code: 'TEMPORARILY_UNAVAILABLE', message: 'unable to verify group membership' };
|
|
175
|
+
}
|
|
176
|
+
if (!members.ok)
|
|
177
|
+
throw { code: 'TEMPORARILY_UNAVAILABLE', message: 'unable to verify group membership' };
|
|
178
|
+
total = Number(members.total || 0);
|
|
179
|
+
actor = members.members.find(member => member.aid === subject.actorId);
|
|
180
|
+
if (actor || members.members.length === 0 || page * pageSize >= total)
|
|
181
|
+
break;
|
|
182
|
+
page += 1;
|
|
183
|
+
} while (page <= 50);
|
|
184
|
+
if (!actor)
|
|
185
|
+
throw { code: 'INVALID_CONTEXT', message: 'authenticated actor is not a member of target group' };
|
|
186
|
+
const peerKey = formatPeerKey('aun', canonicalGroupId);
|
|
187
|
+
const relationConfig = readConfig(ConfigTarget.Relation, { self: subject.selfAid, peerKey }, { cache: false });
|
|
188
|
+
const baselineRole = typeof relationConfig?.role === 'string'
|
|
189
|
+
? relationConfig.role
|
|
190
|
+
: (readRolesConfig(subject.selfAid).defaultRoles?.group ?? null);
|
|
191
|
+
const rebuilt = buildAuthSubject({
|
|
192
|
+
selfAid: subject.selfAid,
|
|
193
|
+
actorId: subject.actorId,
|
|
194
|
+
channel: subject.channel,
|
|
195
|
+
channelType: transport,
|
|
196
|
+
channelId: canonicalGroupId,
|
|
197
|
+
chatType: 'group',
|
|
198
|
+
conversationId: canonicalGroupId,
|
|
199
|
+
fromControlChannel: subject.fromControlChannel,
|
|
200
|
+
identity: subject.isDaemonOwner ? subject.identity : undefined,
|
|
201
|
+
});
|
|
202
|
+
const bound = { ...rebuilt, channel: subject.channel, channelId: canonicalGroupId, conversationId: canonicalGroupId, peerKey, isDaemonOwner: subject.isDaemonOwner || rebuilt.isDaemonOwner, fromControlChannel: subject.fromControlChannel };
|
|
203
|
+
const access = authorizeAccess(bound);
|
|
204
|
+
if (!access.allow)
|
|
205
|
+
throw { code: access.code, message: access.reason };
|
|
206
|
+
return makeContext({ ...input, channelId: canonicalGroupId }, requested, bound, canonicalGroupId, actor.role, baselineRole);
|
|
207
|
+
}
|
|
208
|
+
function makeContext(input, kind, subject, channelId, groupMembershipRole, baselineRole) {
|
|
209
|
+
const descriptor = { kind, channelScope: input.channelScope, transport: input.channelType || subject.channelType, chatType: subject.chatType, self: subject.selfAid, actor: subject.actorId, groupId: kind === 'group_relation' ? subject.conversationId : undefined, peerKey: subject.peerKey };
|
|
210
|
+
return { ...descriptor, channelScope: input.channelScope, requesterRole: subject.role, ...(groupMembershipRole ? { groupMembershipRole } : {}), ...(baselineRole !== undefined ? { baselineRole } : {}), contextKey: hash(descriptor), subject, channelId };
|
|
211
|
+
}
|
|
212
|
+
function bindingArgs(binding, context) {
|
|
213
|
+
const args = { ...(binding.args ?? {}) };
|
|
214
|
+
if (context.kind === 'private_relation' || context.kind === 'group_relation') {
|
|
215
|
+
if (args.scope !== 'role')
|
|
216
|
+
args.scope = 'relation';
|
|
217
|
+
args.self = context.self;
|
|
218
|
+
args.peer = context.peerKey;
|
|
219
|
+
args.peerKey = context.peerKey;
|
|
220
|
+
if (binding.cmd === '/chatmode' && args.field === undefined) {
|
|
221
|
+
args.field = context.kind === 'group_relation' ? 'group' : 'private';
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (context.kind === 'group_relation') {
|
|
225
|
+
args.groupId = context.groupId;
|
|
226
|
+
}
|
|
227
|
+
if (context.kind === 'agent') {
|
|
228
|
+
args.scope = 'agent';
|
|
229
|
+
args.self = context.self;
|
|
230
|
+
}
|
|
231
|
+
return args;
|
|
232
|
+
}
|
|
233
|
+
export function bindMenuExecutionContextArgs(args, context) {
|
|
234
|
+
if (!args && context.kind !== 'group_relation')
|
|
235
|
+
return args;
|
|
236
|
+
const { context: _context, ...bound } = args ?? {};
|
|
237
|
+
if (context.kind === 'private_relation' || context.kind === 'group_relation') {
|
|
238
|
+
if (bound.scope !== 'role')
|
|
239
|
+
bound.scope = 'relation';
|
|
240
|
+
bound.self = context.self;
|
|
241
|
+
bound.peer = context.peerKey;
|
|
242
|
+
bound.peerKey = context.peerKey;
|
|
243
|
+
}
|
|
244
|
+
if (context.kind === 'group_relation')
|
|
245
|
+
bound.groupId = context.groupId;
|
|
246
|
+
if (context.kind === 'agent') {
|
|
247
|
+
bound.scope = 'agent';
|
|
248
|
+
bound.self = context.self;
|
|
249
|
+
}
|
|
250
|
+
return bound;
|
|
251
|
+
}
|
|
252
|
+
export async function resolveExplicitMenuExecutionContext(input) {
|
|
253
|
+
if (!input.args || !Object.prototype.hasOwnProperty.call(input.args, 'context')) {
|
|
254
|
+
return { subject: input.subject, channelId: input.channelId, args: input.args };
|
|
255
|
+
}
|
|
256
|
+
const context = await resolveMenuCatalogContext({
|
|
257
|
+
subject: input.subject,
|
|
258
|
+
channelType: input.channelType,
|
|
259
|
+
channelScope: input.channelScope,
|
|
260
|
+
channelId: input.channelId,
|
|
261
|
+
request: input.args.context,
|
|
262
|
+
directory: input.directory,
|
|
263
|
+
});
|
|
264
|
+
return {
|
|
265
|
+
subject: context.subject,
|
|
266
|
+
channelId: context.channelId,
|
|
267
|
+
args: bindMenuExecutionContextArgs(input.args, context),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function entryVisible(entry, context) { return entry.contexts.includes(context.kind); }
|
|
271
|
+
function intentFor(binding, context) { return { operation: binding.operation, scope: binding.scope, source: 'menu', args: { ...bindingArgs(binding, context) } }; }
|
|
272
|
+
async function operationView(binding, context) {
|
|
273
|
+
if (!binding)
|
|
274
|
+
return undefined;
|
|
275
|
+
if (binding.supported === false)
|
|
276
|
+
return { supported: false, allowed: false, reason: { code: 'NOT_SUPPORTED', message: 'operation is not supported' } };
|
|
277
|
+
if (binding.operation === 'group.rulespolicy.update' && context.subject.role !== 'owner' && !context.subject.isDaemonOwner) {
|
|
278
|
+
return { supported: true, allowed: false, reason: { code: 'PERMISSION_DENIED', message: 'rulesPolicy can only be changed by an Agent owner' } };
|
|
279
|
+
}
|
|
280
|
+
const decision = authorizeOperation({ source: 'menu', intent: intentFor(binding, context), subject: context.subject, audit: false });
|
|
281
|
+
return { supported: true, allowed: decision.allow, ...(binding.mode ? { mode: binding.mode } : {}), ...(binding.items ? { items: binding.items } : {}), ...(binding.dependsOn ? { dependsOn: binding.dependsOn } : {}), ...(decision.allow ? {} : { reason: { code: decision.code, message: decision.reason } }) };
|
|
282
|
+
}
|
|
283
|
+
function wireBinding(binding, context) {
|
|
284
|
+
if (!binding || binding.supported === false)
|
|
285
|
+
return undefined;
|
|
286
|
+
return {
|
|
287
|
+
args: context.kind === 'group_relation'
|
|
288
|
+
? {
|
|
289
|
+
...bindingArgs(binding, context),
|
|
290
|
+
context: { kind: context.kind, ...(context.groupId ? { groupId: context.groupId } : {}) },
|
|
291
|
+
}
|
|
292
|
+
: bindingArgs(binding, context),
|
|
293
|
+
...(binding.action ? { action: binding.action } : {}),
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
export async function getMenuCatalog(input) {
|
|
297
|
+
const context = await resolveMenuCatalogContext(input);
|
|
298
|
+
const entries = [];
|
|
299
|
+
for (const entry of ENTRIES.filter(candidate => entryVisible(candidate, context)).sort((a, b) => a.order - b.order)) {
|
|
300
|
+
const operations = {};
|
|
301
|
+
for (const [key, binding] of [['query', entry.query], ['options', entry.options], ['update', entry.update], ['action', entry.action]]) {
|
|
302
|
+
const view = await operationView(binding, context);
|
|
303
|
+
if (view)
|
|
304
|
+
operations[key] = view;
|
|
305
|
+
}
|
|
306
|
+
entries.push({ id: entry.id, type: entry.type, name: entry.name, label: entry.label, section: entry.section, order: entry.order, target: { scope: context.kind === 'agent' ? 'agent' : context.kind === 'process' ? 'process' : 'relation', ...(context.self ? { self: context.self } : {}), ...(context.peerKey ? { peerKey: context.peerKey } : {}) }, valueSchema: entry.valueSchema ?? { type: 'object' }, operations, wire: { name: entry.name, operations: { ...(wireBinding(entry.query, context) ? { query: wireBinding(entry.query, context) } : {}), ...(wireBinding(entry.options, context) ? { options: wireBinding(entry.options, context) } : {}), ...(wireBinding(entry.update, context) ? { update: wireBinding(entry.update, context) } : {}), ...(wireBinding(entry.action, context) ? { action: wireBinding(entry.action, context) } : {}) } }, ui: { control: entry.control ?? 'action' } });
|
|
307
|
+
}
|
|
308
|
+
const policyRevision = hash({ role: context.subject.role, self: context.subject.selfAid, peerKey: context.subject.peerKey, groupMembershipRole: context.groupMembershipRole, baselineRole: context.baselineRole, roles: readRolesConfig(context.subject.selfAid) });
|
|
309
|
+
const data = { $schema_version: 1, kind: 'menu.catalog', complete: true, catalogRevision: hash({ entries, context: context.contextKey, policyRevision }), policyRevision, generatedAt: Date.now(), context: { contextKey: context.contextKey, kind: context.kind, channelScope: context.channelScope, transport: context.transport, chatType: context.chatType, ...(context.self ? { self: context.self } : {}), ...(context.actor ? { actor: context.actor } : {}), ...(context.groupId ? { groupId: context.groupId } : {}), ...(context.peerKey ? { peerKey: context.peerKey } : {}), requesterRole: context.requesterRole, ...(context.groupMembershipRole ? { groupMembershipRole: context.groupMembershipRole } : {}), ...(context.baselineRole !== undefined ? { baselineRole: context.baselineRole } : {}) }, features: { batchValuesV1: true, verifiedExplicitGroupContext: context.kind === 'group_relation' }, entries };
|
|
310
|
+
return data;
|
|
311
|
+
}
|
|
312
|
+
export async function execMenuValueBatch(input) {
|
|
313
|
+
assertMenuSchemaVersion(input.schemaVersion);
|
|
314
|
+
if (input.consistency !== undefined && input.consistency !== 'best-effort') {
|
|
315
|
+
throw { code: 'NOT_SUPPORTED', message: 'Only best-effort batch consistency is supported' };
|
|
316
|
+
}
|
|
317
|
+
const context = await resolveMenuCatalogContext(input);
|
|
318
|
+
if (!Array.isArray(input.items) || input.items.length < 1 || input.items.length > 32)
|
|
319
|
+
throw { code: 'INVALID_ARGUMENT', message: 'items must contain 1 to 32 entries' };
|
|
320
|
+
const seen = new Set();
|
|
321
|
+
const normalizedItems = [];
|
|
322
|
+
for (const raw of input.items) {
|
|
323
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
324
|
+
throw { code: 'INVALID_ARGUMENT', message: 'each batch item must be an object' };
|
|
325
|
+
const item = raw;
|
|
326
|
+
const keys = Object.keys(item);
|
|
327
|
+
if (keys.some(key => key !== 'itemId' && key !== 'entryId'))
|
|
328
|
+
throw { code: 'INVALID_ARGUMENT', message: 'batch items may only contain itemId and entryId' };
|
|
329
|
+
const itemId = typeof item.itemId === 'string' ? item.itemId.trim() : '';
|
|
330
|
+
const entryId = typeof item.entryId === 'string' ? item.entryId.trim() : '';
|
|
331
|
+
if (!itemId || !entryId || seen.has(itemId))
|
|
332
|
+
throw { code: 'INVALID_ARGUMENT', message: 'itemId and entryId must be non-empty and itemId must be unique' };
|
|
333
|
+
seen.add(itemId);
|
|
334
|
+
normalizedItems.push({ itemId, entryId });
|
|
335
|
+
}
|
|
336
|
+
const results = new Array(normalizedItems.length);
|
|
337
|
+
const byId = new Map(ENTRIES.map(entry => [entry.id, entry]));
|
|
338
|
+
let cursor = 0;
|
|
339
|
+
const resolveItem = async (item) => {
|
|
340
|
+
const { itemId, entryId } = item;
|
|
341
|
+
const entry = byId.get(entryId);
|
|
342
|
+
if (!entry || !entryVisible(entry, context) || !entry.query)
|
|
343
|
+
return { itemId, entryId, ok: false, error: { code: 'NOT_SUPPORTED', message: 'entry is not queryable in this context' } };
|
|
344
|
+
const allowed = await operationView(entry.query, context);
|
|
345
|
+
if (allowed?.supported === false || !(allowed?.allowed))
|
|
346
|
+
return { itemId, entryId, ok: false, error: allowed?.reason ?? { code: 'NOT_SUPPORTED', message: 'operation is not supported or denied' } };
|
|
347
|
+
try {
|
|
348
|
+
const result = await input.execQuery(entry.query.cmd, context.channelId, bindingArgs(entry.query, context), context);
|
|
349
|
+
return !('error' in result) ? { itemId, entryId, ok: true, data: result.data } : { itemId, entryId, ok: false, error: { code: normalizeMenuError({ code: result.code, message: result.error, data: result.data }).code, message: result.error, ...(result.data !== undefined ? { data: result.data } : {}) } };
|
|
350
|
+
}
|
|
351
|
+
catch (error) {
|
|
352
|
+
const normalized = normalizeMenuError(error);
|
|
353
|
+
return { itemId, entryId, ok: false, error: normalized };
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
const worker = async () => {
|
|
357
|
+
while (true) {
|
|
358
|
+
const index = cursor++;
|
|
359
|
+
if (index >= normalizedItems.length)
|
|
360
|
+
return;
|
|
361
|
+
results[index] = await resolveItem(normalizedItems[index]);
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
await Promise.all(Array.from({ length: Math.min(6, normalizedItems.length) }, () => worker()));
|
|
365
|
+
return { $schema_version: 1, kind: 'menu.value_batch', consistency: 'best-effort', observedAt: Date.now(), context: { contextKey: context.contextKey, kind: context.kind, ...(context.self ? { self: context.self } : {}), ...(context.peerKey ? { peerKey: context.peerKey } : {}) }, items: results };
|
|
366
|
+
}
|