evolcore 0.0.11 → 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 +22 -0
- package/bin/codex-managed-hook.mjs +80 -0
- package/dist/agents/claude-runner.js +35 -2
- package/dist/agents/codex-app-server-client.js +120 -14
- package/dist/agents/codex-runner.js +273 -35
- package/dist/agents/ecagent-runner.js +36 -4
- package/dist/agents/gemini-runner.js +1 -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/channels/aun.js +288 -108
- package/dist/channels/daemon.js +13 -0
- package/dist/cli/agent-command.js +56 -8
- package/dist/cli/agent.js +170 -6
- package/dist/cli/aun-commands.js +270 -69
- package/dist/cli/bench.js +12 -3
- package/dist/cli/daemon-commands.js +31 -31
- package/dist/cli/fs-command.js +60 -0
- package/dist/cli/handoff-command.js +3 -0
- package/dist/cli/index.js +30 -2
- 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 +4 -0
- package/dist/config/contact-book-store.js +11 -3
- package/dist/config/contact-request-service.js +161 -15
- package/dist/config/mention-mode.js +8 -24
- package/dist/core/auth/agent-delegation.js +0 -1
- package/dist/core/auth/operation-authorizer.js +30 -7
- package/dist/core/auth/operation-catalog.js +187 -25
- package/dist/core/bootstrap-messages.js +18 -0
- package/dist/core/bootstrap-service.js +63 -4
- 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 +1 -1
- 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 +147 -35
- 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 +41 -21
- package/dist/core/daemon-file-cache.js +2 -1
- package/dist/core/data-migration.js +9 -2
- package/dist/core/evolagent.js +4 -2
- 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/message/logical-queue-bridge.js +1 -0
- package/dist/core/message/mention-schema.js +126 -0
- package/dist/core/message/message-bridge.js +236 -78
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-queue.js +1 -0
- package/dist/core/message/response-engine.js +54 -36
- package/dist/core/permission/ec-command-parser.js +156 -10
- package/dist/core/permission/sandbox-runtime.js +22 -1
- package/dist/core/permission/tool-policy.js +73 -30
- package/dist/core/protected-paths.js +7 -2
- package/dist/core/session/session-manager.js +21 -2
- package/dist/eck/manifest-engine.js +39 -13
- package/dist/index.js +384 -98
- package/dist/ipc.js +218 -9
- 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/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/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/prompt-loading-architecture.md +1 -2
- package/kits/docs/venues/group.md +1 -1
- package/kits/eck_manifest.json +0 -12
- package/kits/rules/04-relation.md +1 -1
- package/kits/rules/05-venue.md +2 -2
- 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 +22 -0
- package/kits/templates/roles/member.json +27 -0
- package/kits/templates/roles/visitor.json +33 -3
- package/kits/templates/system-fragments/bootstrap.md +1 -1
- package/kits/templates/system-fragments/channel.md +1 -1
- package/kits/templates/system-fragments/session.md +1 -1
- package/package.json +2 -2
- package/dist/core/command/evol-menu-version-gate.js +0 -39
|
@@ -331,6 +331,21 @@ async function actionReviewRequest(req, self, context, decision) {
|
|
|
331
331
|
decision,
|
|
332
332
|
});
|
|
333
333
|
if (reviewed.code === 'approved' || reviewed.code === 'rejected' || reviewed.code === 'blocked') {
|
|
334
|
+
if (reviewed.requestId && reviewed.expiresAt) {
|
|
335
|
+
try {
|
|
336
|
+
await context.notifyContactRequestReviewed?.({
|
|
337
|
+
applicantAid: primaryId,
|
|
338
|
+
requestId: reviewed.requestId,
|
|
339
|
+
expiresAt: reviewed.expiresAt,
|
|
340
|
+
status: reviewed.code === 'approved'
|
|
341
|
+
? 'APPROVED'
|
|
342
|
+
: reviewed.code === 'rejected' ? 'REJECTED' : 'NOT_ALLOWED',
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
// Review state is authoritative; notification delivery is best-effort.
|
|
347
|
+
}
|
|
348
|
+
}
|
|
334
349
|
return ok({
|
|
335
350
|
self,
|
|
336
351
|
action: `${decision}-request`,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import crypto from 'crypto';
|
|
2
2
|
import { ConfigTarget, mutateConfig, read as readConfig, resolveEffectiveFieldWithSource, } from '../../config/config-manager.js';
|
|
3
|
-
import {
|
|
3
|
+
import { normalizeMentionMode } from '../../config/mention-mode.js';
|
|
4
4
|
import { readRolesConfig } from '../../config/roles.js';
|
|
5
5
|
import { groupInfo, groupList } from '../../aun/msg/group.js';
|
|
6
6
|
import { formatPeerKey, parsePeerKey } from '../relation/peer-identity.js';
|
|
@@ -156,9 +156,9 @@ async function resolveTarget(args, context) {
|
|
|
156
156
|
scope,
|
|
157
157
|
selector: { self: context.self, peerKey: formatPeerKey('aun', canonicalGroupId) },
|
|
158
158
|
groupId: canonicalGroupId,
|
|
159
|
-
aunMentionMode:
|
|
160
|
-
?? group.settings?.
|
|
161
|
-
?? group.metadata?.
|
|
159
|
+
aunMentionMode: normalizeMentionMode(group.mention_mode)
|
|
160
|
+
?? normalizeMentionMode(group.settings?.mention_mode)
|
|
161
|
+
?? normalizeMentionMode(group.metadata?.mention_mode),
|
|
162
162
|
};
|
|
163
163
|
}
|
|
164
164
|
function groupIdFromArgs(args, currentGroupId) {
|
|
@@ -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
|
+
}
|