evolcore 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +93 -778
- 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 +198 -48
- package/dist/agents/codex-app-server-client.js +68 -0
- package/dist/agents/codex-runner.js +203 -8
- 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/aun/outbox.js +8 -0
- package/dist/channels/aun.js +241 -116
- package/dist/channels/daemon.js +18 -4
- package/dist/channels/dingtalk.js +1005 -260
- package/dist/channels/feishu.js +184 -56
- package/dist/channels/qqbot.js +23 -1
- package/dist/channels/wechat.js +303 -155
- package/dist/channels/wecom.js +1132 -164
- package/dist/cli/agent-command.js +2 -1
- package/dist/cli/aun-commands.js +98 -43
- package/dist/cli/bench.js +2 -2
- package/dist/cli/contact.js +71 -0
- package/dist/cli/ctl-command.js +2 -2
- package/dist/cli/daemon-commands.js +77 -214
- package/dist/cli/handoff-command.js +4 -3
- package/dist/cli/help.js +9 -5
- package/dist/cli/index.js +132 -116
- package/dist/cli/init-channel.js +242 -129
- package/dist/cli/init.js +70 -31
- package/dist/cli/model.js +2 -1
- package/dist/cli/net-check.js +2 -2
- package/dist/cli/queue-command.js +30 -6
- package/dist/cli/raw-key-input.js +25 -0
- package/dist/cli/response.js +5 -6
- package/dist/cli/restart-monitor.js +25 -1
- package/dist/cli/stats.js +6 -4
- package/dist/cli/trigger-command.js +146 -25
- package/dist/cli/version.js +6 -1
- package/dist/config/access-policy-domain.js +38 -0
- package/dist/config/access-policy.js +134 -0
- package/dist/config/builtin-role-templates.js +42 -17
- package/dist/config/builtin-roles.js +31 -6
- package/dist/config/config-field-policy.js +17 -5
- package/dist/config/config-manager.js +409 -29
- package/dist/config/config-operation-service.js +35 -38
- package/dist/config/contact-alias.js +68 -0
- package/dist/config/contact-bind-code.js +274 -0
- package/dist/config/contact-book-store.js +622 -0
- package/dist/config/contact-book-v2-startup.js +35 -0
- package/dist/config/contact-book.js +185 -300
- package/dist/config/contact-operation-service.js +116 -0
- package/dist/config/contact-request-service.js +331 -0
- package/dist/config/peer-role-resolver.js +136 -55
- package/dist/config/role-ranks.js +18 -0
- package/dist/config/role-service.js +16 -19
- package/dist/config/role-store.js +16 -5
- package/dist/config/roles.js +10 -1
- package/dist/config/schema-registry.js +49 -24
- package/dist/config-store.js +14 -4
- package/dist/core/auth/agent-delegation.js +105 -11
- package/dist/core/auth/authorization-audit.js +16 -1
- package/dist/core/auth/operation-authorizer.js +10 -0
- package/dist/core/auth/operation-catalog.js +105 -1
- 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 +113 -21
- package/dist/core/command/connect-menu.js +599 -0
- 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 +382 -100
- 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 +128 -29
- package/dist/core/command/slash-gate.js +1 -1
- package/dist/core/command/slash-handler.js +186 -49
- package/dist/core/daemon-file-cache.js +40 -6
- package/dist/core/event-catalog.js +99 -0
- package/dist/core/evolagent-registry.js +4 -40
- package/dist/core/evolagent.js +20 -9
- 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 +20 -14
- package/dist/core/message/inbound-admission.js +124 -0
- package/dist/core/message/message-bridge.js +670 -57
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-queue.js +195 -10
- package/dist/core/message/message-utils.js +8 -2
- package/dist/core/message/peer-mode.js +7 -8
- package/dist/core/message/response-engine.js +642 -96
- package/dist/core/message/send-receipt.js +24 -0
- package/dist/core/message/stream-debouncer.js +11 -2
- 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 +115 -38
- package/dist/core/protected-paths.js +38 -11
- package/dist/core/session/session-fs-store.js +44 -3
- package/dist/core/session/session-manager.js +86 -8
- 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 +285 -94
- package/dist/ipc.js +114 -8
- 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/stats/billing.js +20 -8
- package/dist/trigger/manager.js +3 -0
- package/dist/trigger/parser.js +126 -11
- package/dist/trigger/patch.js +12 -2
- package/dist/trigger/scheduler.js +444 -40
- package/dist/trigger/validation.js +13 -0
- package/dist/utils/aid-bind.js +43 -29
- package/dist/utils/error-dict.json +7 -0
- package/dist/utils/evolcore-version.js +21 -0
- package/dist/utils/instance-registry.js +14 -7
- package/dist/utils/log-writer.js +46 -0
- package/dist/utils/logger.js +2 -2
- package/dist/utils/media-cache.js +4 -1
- package/dist/utils/model-prices.jsonl +6 -3
- package/dist/utils/restart-safety.js +31 -0
- package/dist/utils/stable-semver.js +21 -0
- package/dist/utils/stats.js +33 -9
- package/dist/utils/system-memory.js +62 -0
- package/kits/docs/INDEX.md +2 -1
- package/kits/docs/channels/aun.md +4 -13
- package/kits/docs/evolcore/INDEX.md +5 -3
- package/kits/docs/evolcore/agent.md +9 -1
- package/kits/docs/evolcore/aid.md +5 -2
- package/kits/docs/evolcore/config.md +47 -2
- package/kits/docs/evolcore/contact.md +61 -0
- package/kits/docs/evolcore/fs.md +9 -0
- package/kits/docs/evolcore/group-rules.md +46 -4
- package/kits/docs/evolcore/group.md +15 -6
- package/kits/docs/evolcore/model.md +4 -1
- package/kits/docs/evolcore/msg.md +12 -6
- package/kits/docs/evolcore/response.md +16 -21
- package/kits/docs/evolcore/rpc.md +2 -0
- package/kits/docs/evolcore/stats.md +15 -2
- package/kits/docs/evolcore/storage.md +1 -0
- package/kits/docs/evolcore/trigger.md +42 -10
- package/kits/docs/path-registry.md +36 -17
- package/kits/eck_manifest.json +12 -0
- package/kits/eck_message_manifest.json +12 -1
- package/kits/migrations/migrate-contact-book-v2.mjs +754 -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 +21 -7
- package/kits/schemas/agent-config.schema.6.json +322 -0
- package/kits/schemas/agent-config.schema.7.json +303 -0
- package/kits/schemas/agent-config.schema.8.json +304 -0
- package/kits/schemas/agent-config.schema.9.json +374 -0
- package/kits/schemas/contact-book.schema.2.json +43 -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.5.json +47 -0
- package/kits/schemas/relation-config.schema.6.json +46 -0
- package/kits/schemas/relation-config.schema.7.json +59 -0
- package/kits/schemas/role-config.schema.1.json +1 -0
- package/kits/schemas/role-registry.schema.1.json +2 -2
- 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 +47 -0
- package/kits/templates/roles/member.json +5 -0
- package/kits/templates/roles/owner.json +1 -0
- package/kits/templates/roles/visitor.json +5 -0
- package/kits/templates/system-fragments/channel.md +12 -2
- package/kits/templates/system-fragments/commands.md +3 -1
- 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 +7 -5
- package/MIGRATION-0.5.0.md +0 -378
- package/ROLE_ACCESS_CONTROL.md +0 -174
- package/assets/brand/evolcore/README.md +0 -19
- package/assets/brand/evolcore/evolcore-app-icon.png +0 -0
- package/assets/brand/evolcore/evolcore-app-icon.svg +0 -13
- package/assets/brand/evolcore/evolcore-brand-board.png +0 -0
- package/assets/brand/evolcore/evolcore-brand-board.svg +0 -126
- package/assets/brand/evolcore/evolcore-logo-kit.zip +0 -0
- package/assets/brand/evolcore/evolcore-logo-reverse.png +0 -0
- package/assets/brand/evolcore/evolcore-logo-reverse.svg +0 -14
- package/assets/brand/evolcore/evolcore-logo.png +0 -0
- package/assets/brand/evolcore/evolcore-logo.svg +0 -14
- package/assets/brand/evolcore/evolcore-mark.png +0 -0
- package/assets/brand/evolcore/evolcore-mark.svg +0 -10
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'crypto';
|
|
2
2
|
export const MENU_REQUEST_TYPES = new Set([
|
|
3
|
+
'menu.token.request',
|
|
3
4
|
'menu.list',
|
|
4
5
|
'menu.query',
|
|
5
6
|
'menu.options',
|
|
@@ -12,6 +13,7 @@ const EXPLICIT_SCOPE_UPDATE_NAMES = new Set([
|
|
|
12
13
|
'chatmode',
|
|
13
14
|
'dispatch',
|
|
14
15
|
'mentionmode',
|
|
16
|
+
'group',
|
|
15
17
|
'permission',
|
|
16
18
|
]);
|
|
17
19
|
const EXPLICIT_SCOPE_UPDATE_COMMANDS = new Set([
|
|
@@ -20,6 +22,7 @@ const EXPLICIT_SCOPE_UPDATE_COMMANDS = new Set([
|
|
|
20
22
|
'/chatmode',
|
|
21
23
|
'/dispatch',
|
|
22
24
|
'/mentionmode',
|
|
25
|
+
'/group',
|
|
23
26
|
'/perm',
|
|
24
27
|
]);
|
|
25
28
|
function firstCommandToken(value) {
|
|
@@ -78,6 +81,8 @@ export function validateMenuRequest(request) {
|
|
|
78
81
|
if (!MENU_REQUEST_TYPES.has(type)) {
|
|
79
82
|
return { code: 'METHOD_NOT_FOUND', message: `Unknown menu request type: ${type}` };
|
|
80
83
|
}
|
|
84
|
+
if (type === 'menu.token.request')
|
|
85
|
+
return null;
|
|
81
86
|
const hasName = typeof request.name === 'string' && request.name.trim().length > 0;
|
|
82
87
|
const hasCompatCmd = typeof request.cmd === 'string' && request.cmd.trim().length > 0;
|
|
83
88
|
if (type !== 'menu.list' && !hasName && !hasCompatCmd) {
|
|
@@ -144,8 +149,9 @@ const INVALID_ARGUMENT_CODES = new Set([
|
|
|
144
149
|
const STABLE_CODES = new Set([
|
|
145
150
|
'ROLE_ACCESS_DENIED', 'PERMISSION_DENIED', 'UNAUTHORIZED', 'FORBIDDEN',
|
|
146
151
|
'INVALID_ARGUMENT', 'NOT_ALLOWED', 'NOT_SUPPORTED', 'UNSUPPORTED',
|
|
147
|
-
'METHOD_NOT_FOUND', 'NOT_FOUND', 'CONFLICT', 'EXECUTION_TIMEOUT',
|
|
148
|
-
'TEMPORARILY_UNAVAILABLE', 'INTERNAL_ERROR', 'MISSING_SCOPE',
|
|
152
|
+
'METHOD_NOT_FOUND', 'NOT_FOUND', 'CONFLICT', 'EXPIRED', 'EXECUTION_TIMEOUT',
|
|
153
|
+
'TEMPORARILY_UNAVAILABLE', 'INTERNAL_ERROR', 'MISSING_SCOPE', 'UPGRADE_REQUIRED',
|
|
154
|
+
'MENU_TOKEN_REQUIRED', 'MENU_TOKEN_REJECTED', 'MENU_TOKEN_ENCRYPTION_REQUIRED',
|
|
149
155
|
]);
|
|
150
156
|
export function normalizeMenuError(error) {
|
|
151
157
|
const source = error && typeof error === 'object' ? error : {};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { createHash, randomBytes, timingSafeEqual } from 'crypto';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { agentDataDir } from '../../paths.js';
|
|
4
|
+
import { atomicReadJson, atomicWriteJson } from '../../utils/atomic-write.js';
|
|
5
|
+
export const DEFAULT_MENU_TOKEN_IDLE_TIMEOUT_SECONDS = 30 * 24 * 60 * 60;
|
|
6
|
+
export const MAX_MENU_TOKENS_PER_AGENT = 1_000;
|
|
7
|
+
function hashToken(token) {
|
|
8
|
+
return createHash('sha256').update(token).digest('hex');
|
|
9
|
+
}
|
|
10
|
+
function isTokenRecord(value) {
|
|
11
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
12
|
+
return false;
|
|
13
|
+
const record = value;
|
|
14
|
+
return typeof record.peerAid === 'string'
|
|
15
|
+
&& record.peerAid.trim().length > 0
|
|
16
|
+
&& typeof record.tokenHash === 'string'
|
|
17
|
+
&& /^[a-f0-9]{64}$/.test(record.tokenHash)
|
|
18
|
+
&& typeof record.requestedAt === 'number'
|
|
19
|
+
&& Number.isFinite(record.requestedAt)
|
|
20
|
+
&& record.requestedAt >= 0
|
|
21
|
+
&& typeof record.lastUsedAt === 'number'
|
|
22
|
+
&& Number.isFinite(record.lastUsedAt)
|
|
23
|
+
&& record.lastUsedAt >= 0
|
|
24
|
+
&& typeof record.idleTimeout === 'number'
|
|
25
|
+
&& Number.isInteger(record.idleTimeout)
|
|
26
|
+
&& record.idleTimeout > 0;
|
|
27
|
+
}
|
|
28
|
+
function isExpired(record, now) {
|
|
29
|
+
return now - record.lastUsedAt >= record.idleTimeout * 1000;
|
|
30
|
+
}
|
|
31
|
+
function hashesEqual(left, right) {
|
|
32
|
+
const leftBytes = Buffer.from(left, 'hex');
|
|
33
|
+
const rightBytes = Buffer.from(right, 'hex');
|
|
34
|
+
return leftBytes.length === rightBytes.length && timingSafeEqual(leftBytes, rightBytes);
|
|
35
|
+
}
|
|
36
|
+
export function menuTokenStorePath(selfAid) {
|
|
37
|
+
return path.join(agentDataDir(selfAid), 'menu-tokens.json');
|
|
38
|
+
}
|
|
39
|
+
export class MenuTokenStore {
|
|
40
|
+
maxRecords;
|
|
41
|
+
constructor(maxRecords = MAX_MENU_TOKENS_PER_AGENT) {
|
|
42
|
+
this.maxRecords = maxRecords;
|
|
43
|
+
}
|
|
44
|
+
issue(selfAid, peerAid, metadata = {}, now = Date.now()) {
|
|
45
|
+
const file = this.read(selfAid);
|
|
46
|
+
const idleTimeout = DEFAULT_MENU_TOKEN_IDLE_TIMEOUT_SECONDS;
|
|
47
|
+
const tokens = file.tokens.filter(record => record.peerAid !== peerAid && !isExpired(record, now));
|
|
48
|
+
if (tokens.length >= this.maxRecords) {
|
|
49
|
+
if (tokens.length !== file.tokens.length)
|
|
50
|
+
this.write(selfAid, tokens);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const menuToken = `mt_${randomBytes(32).toString('base64url')}`;
|
|
54
|
+
tokens.push({
|
|
55
|
+
peerAid,
|
|
56
|
+
tokenHash: hashToken(menuToken),
|
|
57
|
+
requestedAt: now,
|
|
58
|
+
lastUsedAt: now,
|
|
59
|
+
idleTimeout,
|
|
60
|
+
...(metadata.clientName ? { clientName: metadata.clientName } : {}),
|
|
61
|
+
...(metadata.clientVersion ? { clientVersion: metadata.clientVersion } : {}),
|
|
62
|
+
...(metadata.evolVersion ? { evolVersion: metadata.evolVersion } : {}),
|
|
63
|
+
});
|
|
64
|
+
this.write(selfAid, tokens);
|
|
65
|
+
return { menuToken, idleTimeout };
|
|
66
|
+
}
|
|
67
|
+
use(selfAid, peerAid, menuToken, now = Date.now()) {
|
|
68
|
+
const file = this.read(selfAid);
|
|
69
|
+
const activeTokens = file.tokens.filter(record => !isExpired(record, now));
|
|
70
|
+
const tokenHash = typeof menuToken === 'string' && menuToken.length > 0 && menuToken.length <= 256
|
|
71
|
+
? hashToken(menuToken)
|
|
72
|
+
: undefined;
|
|
73
|
+
const matchingIndex = tokenHash
|
|
74
|
+
? activeTokens.findIndex(record => record.peerAid === peerAid && hashesEqual(record.tokenHash, tokenHash))
|
|
75
|
+
: -1;
|
|
76
|
+
if (matchingIndex < 0) {
|
|
77
|
+
if (activeTokens.length !== file.tokens.length)
|
|
78
|
+
this.write(selfAid, activeTokens);
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
activeTokens[matchingIndex] = { ...activeTokens[matchingIndex], lastUsedAt: now };
|
|
82
|
+
this.write(selfAid, activeTokens);
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
read(selfAid) {
|
|
86
|
+
try {
|
|
87
|
+
const value = atomicReadJson(menuTokenStorePath(selfAid));
|
|
88
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
89
|
+
return { schemaVersion: 1, tokens: [] };
|
|
90
|
+
const file = value;
|
|
91
|
+
if (file.schemaVersion !== 1 || !Array.isArray(file.tokens))
|
|
92
|
+
return { schemaVersion: 1, tokens: [] };
|
|
93
|
+
return { schemaVersion: 1, tokens: file.tokens.filter(isTokenRecord) };
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return { schemaVersion: 1, tokens: [] };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
write(selfAid, tokens) {
|
|
100
|
+
atomicWriteJson(menuTokenStorePath(selfAid), { schemaVersion: 1, tokens });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -3,8 +3,8 @@ import path from 'path';
|
|
|
3
3
|
import crypto from 'crypto';
|
|
4
4
|
import { ConfigTarget, read, resolveEffectiveWithSources, write } from '../../config/config-manager.js';
|
|
5
5
|
import { getBuiltinRolesConfig, getRoleDefinition, isValidUserRoleName, readRolesConfig, } from '../../config/roles.js';
|
|
6
|
-
import { BUILTIN_USER_ROLES, MANAGEMENT_ROLES, isManagementRole } from '../../config/builtin-roles.js';
|
|
7
|
-
import { checkRoleAccess,
|
|
6
|
+
import { BUILTIN_USER_ROLES, MANAGEMENT_ROLES, expectedRoleRank, isManagementRole } from '../../config/builtin-roles.js';
|
|
7
|
+
import { checkRoleAccess, isStaticAgentOwner, resolvePeerRoleDetail } from '../../config/peer-role-resolver.js';
|
|
8
8
|
import { relationFieldWriteOperation, resolveRoleFieldPermission, } from '../../config/config-field-policy.js';
|
|
9
9
|
import { evaluateRoleOperationCapability } from '../auth/operation-authorizer.js';
|
|
10
10
|
import { listOperations } from '../auth/operation-catalog.js';
|
|
@@ -55,8 +55,10 @@ export function roleMenuOperation(kind, args, value) {
|
|
|
55
55
|
}
|
|
56
56
|
if (kind === 'action')
|
|
57
57
|
return 'role.policy.write';
|
|
58
|
-
if (args?.resource !== 'assignment')
|
|
58
|
+
if (args?.resource !== 'assignment' && args?.resource !== 'admin')
|
|
59
59
|
return 'role.policy.write';
|
|
60
|
+
if (args?.resource === 'admin')
|
|
61
|
+
return 'role.assign';
|
|
60
62
|
try {
|
|
61
63
|
return JSON.parse(value ?? '') === null ? 'role.revoke' : 'role.assign';
|
|
62
64
|
}
|
|
@@ -168,9 +170,11 @@ export async function roleMenuUpdate(context, args, rawValue) {
|
|
|
168
170
|
result = updateDefinition(context.self, args, value);
|
|
169
171
|
}
|
|
170
172
|
else if (resource === 'defaultRoles')
|
|
171
|
-
result = updateDefaultRoles(context
|
|
173
|
+
result = updateDefaultRoles(context, args, value);
|
|
172
174
|
else if (resource === 'assignment')
|
|
173
175
|
result = await updateAssignment(context, args, value);
|
|
176
|
+
else if (resource === 'admin')
|
|
177
|
+
result = await updateAdminAssignment(context, args, value);
|
|
174
178
|
else
|
|
175
179
|
throw roleError('NOT_SUPPORTED', 'The requested Role resource is not supported', context.self, { resource: resource || null });
|
|
176
180
|
auditMutation(context, args, before, result, resource === 'defaultRoles' ? 'default-update' : resource === 'assignment' ? 'assignment' : 'update');
|
|
@@ -260,6 +264,8 @@ function auditMutation(context, args, before, result, operation) {
|
|
|
260
264
|
const afterRelation = relationFile ? afterFiles[relationFile] : undefined;
|
|
261
265
|
const roleId = (stringArg(args?.roleId) || undefined)
|
|
262
266
|
?? result?.roleId
|
|
267
|
+
?? result?.assignment?.explicitRole
|
|
268
|
+
?? result?.assignment?.explicitDefaultRole
|
|
263
269
|
?? (isObject(afterRelation) && typeof afterRelation.role === 'string' ? afterRelation.role : undefined)
|
|
264
270
|
?? (isObject(beforeRelation) && typeof beforeRelation.role === 'string' ? beforeRelation.role : undefined);
|
|
265
271
|
const primaryFile = before.relation
|
|
@@ -300,7 +306,7 @@ export function recoverAllRoleMutationsSync() {
|
|
|
300
306
|
if (!fs.existsSync(agentsDir))
|
|
301
307
|
return;
|
|
302
308
|
for (const entry of fs.readdirSync(agentsDir, { withFileTypes: true })) {
|
|
303
|
-
if (!entry.isDirectory())
|
|
309
|
+
if (!entry.isDirectory() || !isValidAid(entry.name))
|
|
304
310
|
continue;
|
|
305
311
|
if (!fs.existsSync(roleJournalPath(entry.name)))
|
|
306
312
|
continue;
|
|
@@ -447,7 +453,8 @@ function updateDefinition(self, args, value) {
|
|
|
447
453
|
...(args?.remove ? { removed: normalizeRemove(args.remove) } : {}),
|
|
448
454
|
};
|
|
449
455
|
}
|
|
450
|
-
function updateDefaultRoles(
|
|
456
|
+
function updateDefaultRoles(context, args, value) {
|
|
457
|
+
const self = context.self;
|
|
451
458
|
if (!isObject(value))
|
|
452
459
|
throw invalidJsonType(self, 'defaultRoles', 'object');
|
|
453
460
|
assertOnlyKeys(value, ['private', 'group'], 'defaultRoles', self);
|
|
@@ -460,6 +467,13 @@ function updateDefaultRoles(self, args, value) {
|
|
|
460
467
|
};
|
|
461
468
|
assertPolicyRevision(self, args);
|
|
462
469
|
const registry = readRoleRegistry(self);
|
|
470
|
+
const changesAdminDefault = ['private', 'group'].some(scene => registry.defaultRoles[scene] !== nextDefaults[scene]
|
|
471
|
+
&& (registry.defaultRoles[scene] === 'admin' || nextDefaults[scene] === 'admin'));
|
|
472
|
+
if (context.actorRole !== 'owner' && changesAdminDefault) {
|
|
473
|
+
throw roleError('NOT_ALLOWED', 'Only an owner can set or replace an admin default role', self, {
|
|
474
|
+
kind: 'admin_owner_only',
|
|
475
|
+
});
|
|
476
|
+
}
|
|
463
477
|
const nextRegistry = { ...registry, defaultRoles: nextDefaults };
|
|
464
478
|
const changed = stableStringify(registry) !== stableStringify(nextRegistry);
|
|
465
479
|
if (changed)
|
|
@@ -484,30 +498,79 @@ async function updateAssignment(context, args, value) {
|
|
|
484
498
|
if (roleId !== null)
|
|
485
499
|
target = await validateTargetExists(context, target);
|
|
486
500
|
const targetAid = target.kind === 'private' ? target.peerAid : null;
|
|
487
|
-
if (targetAid &&
|
|
488
|
-
throw roleError('NOT_ALLOWED', '
|
|
489
|
-
kind: '
|
|
501
|
+
if (targetAid && isStaticAgentOwner(self, targetAid)) {
|
|
502
|
+
throw roleError('NOT_ALLOWED', 'Owner assignments cannot be changed through relations', self, {
|
|
503
|
+
kind: 'owner_protected',
|
|
490
504
|
targetAid,
|
|
491
505
|
});
|
|
492
506
|
}
|
|
493
507
|
const current = readRelation(target.peerKey, self);
|
|
508
|
+
if (context.actorRole !== 'owner' && (roleId === 'admin' || current?.config.role === 'admin')) {
|
|
509
|
+
throw roleError('NOT_ALLOWED', 'Only an owner can grant or revoke admin', self, {
|
|
510
|
+
kind: 'admin_owner_only',
|
|
511
|
+
target: target.peerKey,
|
|
512
|
+
});
|
|
513
|
+
}
|
|
494
514
|
assertRelationRevision(self, args, current?.revision ?? null, target.peerKey);
|
|
495
|
-
if (!current && roleId === null)
|
|
515
|
+
if (!current && roleId === null)
|
|
496
516
|
return assignmentResponse(self, target, null, false, null);
|
|
497
|
-
}
|
|
498
517
|
const relation = clone(current?.config ?? { $schema_version: currentVersion('relation-config') });
|
|
499
|
-
relation.targetKind = target.kind
|
|
518
|
+
relation.targetKind = target.kind;
|
|
500
519
|
if (roleId === null)
|
|
501
520
|
delete relation.role;
|
|
502
521
|
else
|
|
503
522
|
relation.role = roleId;
|
|
504
|
-
const normalized = normalizeRelationForScene(relation, target.kind
|
|
523
|
+
const normalized = normalizeRelationForScene(relation, target.kind);
|
|
505
524
|
const changed = stableStringify(current?.config ?? null) !== stableStringify(normalized);
|
|
506
525
|
if (changed)
|
|
507
526
|
write(ConfigTarget.Relation, normalized, { self, peerKey: target.peerKey });
|
|
508
527
|
const nextRevision = changed ? revision(normalized) : current?.revision ?? null;
|
|
509
528
|
return assignmentResponse(self, target, normalized, changed, nextRevision);
|
|
510
529
|
}
|
|
530
|
+
async function updateAdminAssignment(context, args, value) {
|
|
531
|
+
const self = context.self;
|
|
532
|
+
if (context.actorRole !== 'owner') {
|
|
533
|
+
throw roleError('NOT_ALLOWED', 'Only an owner can grant or revoke admin', self, { kind: 'admin_owner_only' });
|
|
534
|
+
}
|
|
535
|
+
let target = normalizeTarget(args?.target, self);
|
|
536
|
+
if (target.kind !== 'private')
|
|
537
|
+
throw invalidTargetValue(self, 'target.kind');
|
|
538
|
+
if (isStaticAgentOwner(self, target.peerAid)) {
|
|
539
|
+
throw roleError('NOT_ALLOWED', 'Owner assignments cannot be changed through contact admin', self, { kind: 'owner_protected' });
|
|
540
|
+
}
|
|
541
|
+
const enabled = value === true || value === 'grant'
|
|
542
|
+
? true
|
|
543
|
+
: value === false || value === null || value === 'revoke'
|
|
544
|
+
? false
|
|
545
|
+
: undefined;
|
|
546
|
+
if (enabled === undefined)
|
|
547
|
+
throw invalidJsonType(self, 'admin', 'boolean, grant, revoke, or null');
|
|
548
|
+
if (enabled)
|
|
549
|
+
target = await validateTargetExists(context, target);
|
|
550
|
+
const current = readRelation(target.peerKey, self);
|
|
551
|
+
assertRelationRevision(self, args, current?.revision ?? null, target.peerKey);
|
|
552
|
+
if (!current && !enabled) {
|
|
553
|
+
return {
|
|
554
|
+
$schema_version: ROLE_MENU_SCHEMA_VERSION, self, resource: 'admin', target,
|
|
555
|
+
changed: false, relationRevision: null, admin: false,
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
const relation = clone(current?.config ?? { $schema_version: currentVersion('relation-config') });
|
|
559
|
+
relation.targetKind = 'private';
|
|
560
|
+
if (enabled)
|
|
561
|
+
relation.role = 'admin';
|
|
562
|
+
else if (relation.role === 'admin')
|
|
563
|
+
delete relation.role;
|
|
564
|
+
const normalized = normalizeRelationForScene(relation, 'private');
|
|
565
|
+
const changed = stableStringify(current?.config ?? null) !== stableStringify(normalized);
|
|
566
|
+
if (changed)
|
|
567
|
+
write(ConfigTarget.Relation, normalized, { self, peerKey: target.peerKey });
|
|
568
|
+
const nextRevision = changed ? revision(normalized) : current?.revision ?? null;
|
|
569
|
+
return {
|
|
570
|
+
$schema_version: ROLE_MENU_SCHEMA_VERSION, self, resource: 'admin', target,
|
|
571
|
+
changed, relationRevision: nextRevision, admin: normalized.role === 'admin',
|
|
572
|
+
};
|
|
573
|
+
}
|
|
511
574
|
function createRole(self, args) {
|
|
512
575
|
const roleId = requiredRoleId(args, self);
|
|
513
576
|
if (!isValidUserRoleName(roleId)) {
|
|
@@ -520,6 +583,10 @@ function createRole(self, args) {
|
|
|
520
583
|
if (!isObject(args?.definition))
|
|
521
584
|
throw validationFailed(self, roleId, [{ field: 'definition', code: 'INVALID_TYPE', message: 'definition must be an object' }]);
|
|
522
585
|
const definition = clone(args.definition);
|
|
586
|
+
if (definition.rank !== undefined && definition.rank !== expectedRoleRank(roleId)) {
|
|
587
|
+
throw validationFailed(self, roleId, [{ field: 'definition.rank', code: 'INVALID_VALUE', message: 'custom role rank is fixed at 500' }]);
|
|
588
|
+
}
|
|
589
|
+
definition.rank = expectedRoleRank(roleId);
|
|
523
590
|
const errors = validateRoleConfig(definition);
|
|
524
591
|
if (errors.length)
|
|
525
592
|
throw validationFailed(self, roleId, errors);
|
|
@@ -596,6 +663,11 @@ async function deleteRole(context, args) {
|
|
|
596
663
|
const replacement = hasReplacement
|
|
597
664
|
? validateAssignableRole(args?.replacementRoleId, self, 'replacementRoleId')
|
|
598
665
|
: undefined;
|
|
666
|
+
if (replacement === 'admin' && context.actorRole !== 'owner') {
|
|
667
|
+
throw roleError('NOT_ALLOWED', 'Only an owner can replace role assignments with admin', self, {
|
|
668
|
+
kind: 'admin_owner_only',
|
|
669
|
+
});
|
|
670
|
+
}
|
|
599
671
|
if (replacement === roleId) {
|
|
600
672
|
throw validationFailed(self, roleId, [{ field: 'replacementRoleId', code: 'INVALID_VALUE', message: 'replacementRoleId must differ from roleId' }]);
|
|
601
673
|
}
|
|
@@ -646,11 +718,13 @@ async function assignmentInventory(context, args, policyRevision) {
|
|
|
646
718
|
if (args?.roleId !== undefined && !roleFilter)
|
|
647
719
|
throw validationFailed(self, undefined, [{ field: 'roleId', code: 'INVALID_VALUE', message: 'roleId must be a non-empty string' }]);
|
|
648
720
|
const items = [];
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
const
|
|
652
|
-
|
|
653
|
-
|
|
721
|
+
if (args?.includeDefaults !== false) {
|
|
722
|
+
const defaults = effectiveDefaultRoles(self);
|
|
723
|
+
for (const scene of ['private', 'group']) {
|
|
724
|
+
const roleId = defaults[scene];
|
|
725
|
+
if (roleId && (!roleFilter || roleFilter === roleId)) {
|
|
726
|
+
items.push({ target: { kind: 'default', scene }, roleId, relationRevision: null });
|
|
727
|
+
}
|
|
654
728
|
}
|
|
655
729
|
}
|
|
656
730
|
let complete = true;
|
|
@@ -930,11 +1004,13 @@ function normalizeTarget(value, self) {
|
|
|
930
1004
|
const groupId = stringArg(value.groupId);
|
|
931
1005
|
if (!groupId)
|
|
932
1006
|
throw invalidTarget(self, 'target.groupId');
|
|
1007
|
+
if (!isValidAid(groupId) && !isExplicitGroupId(groupId))
|
|
1008
|
+
throw invalidTargetValue(self, 'target.groupId');
|
|
933
1009
|
return { kind, groupId, peerKey: formatPeerKey('aun', groupId) };
|
|
934
1010
|
}
|
|
935
1011
|
throw validationFailed(self, undefined, [{ field: 'target.kind', code: 'INVALID_VALUE', message: 'target.kind must be private or group' }]);
|
|
936
1012
|
}
|
|
937
|
-
function targetRoleView(self, target, relation) {
|
|
1013
|
+
function targetRoleView(self, target, relation = null) {
|
|
938
1014
|
if (target.kind === 'private') {
|
|
939
1015
|
const explicitRole = relation?.role ?? null;
|
|
940
1016
|
const detail = resolvePeerRoleDetail({
|
|
@@ -1086,7 +1162,15 @@ function policyDocument(agent) {
|
|
|
1086
1162
|
function roleMeta(self, roleId) {
|
|
1087
1163
|
if (isManagementRole(roleId)) {
|
|
1088
1164
|
const overridden = fs.existsSync(agentRoleConfig(self, roleId));
|
|
1089
|
-
return {
|
|
1165
|
+
return {
|
|
1166
|
+
origin: 'builtin',
|
|
1167
|
+
overridden,
|
|
1168
|
+
kind: 'management',
|
|
1169
|
+
assignable: roleId === 'admin',
|
|
1170
|
+
editable: true,
|
|
1171
|
+
deletable: false,
|
|
1172
|
+
resettable: true,
|
|
1173
|
+
};
|
|
1090
1174
|
}
|
|
1091
1175
|
const builtin = Object.prototype.hasOwnProperty.call(getBuiltinRolesConfig().roles, roleId);
|
|
1092
1176
|
const overridden = fs.existsSync(agentRoleConfig(self, roleId));
|
|
@@ -1131,7 +1215,7 @@ function parseUpdateValue(rawValue, self) {
|
|
|
1131
1215
|
function validateAssignableRole(value, self, field) {
|
|
1132
1216
|
if (value === null)
|
|
1133
1217
|
return null;
|
|
1134
|
-
if (typeof value !== 'string' || !value.trim() ||
|
|
1218
|
+
if (typeof value !== 'string' || !value.trim() || value === 'owner' || !getRoleDefinition(value, self)) {
|
|
1135
1219
|
throw validationFailed(self, undefined, [{ field, code: 'INVALID_VALUE', message: 'value must be null or an existing assignable user role' }]);
|
|
1136
1220
|
}
|
|
1137
1221
|
return value;
|
|
@@ -1288,11 +1372,15 @@ function validateRoleDomainTransactionChanges(self, registry, changes) {
|
|
|
1288
1372
|
for (const change of changes) {
|
|
1289
1373
|
if (!change.after || !change.file.endsWith('.json') || change.file.endsWith('index.json'))
|
|
1290
1374
|
continue;
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1375
|
+
const isRelation = change.file.includes(`${path.sep}relations${path.sep}`);
|
|
1376
|
+
const schema = loadSchema(isRelation ? 'relation-config' : 'role-config');
|
|
1377
|
+
const value = JSON.parse(change.after);
|
|
1378
|
+
if (!schema.validate(value)) {
|
|
1379
|
+
throw validationFailed(self, undefined, [{
|
|
1380
|
+
field: isRelation ? 'relation' : 'role',
|
|
1381
|
+
code: 'SCHEMA_INVALID',
|
|
1382
|
+
message: isRelation ? 'invalid relation config' : 'invalid role definition',
|
|
1383
|
+
}]);
|
|
1296
1384
|
}
|
|
1297
1385
|
}
|
|
1298
1386
|
}
|
|
@@ -1337,12 +1425,23 @@ function removeJournalFiles(journalFile) {
|
|
|
1337
1425
|
}
|
|
1338
1426
|
function validateJournal(self, journal) {
|
|
1339
1427
|
const root = path.resolve(agentDir(self));
|
|
1340
|
-
if (journal.$schema_version !== 1 || journal.self !== self
|
|
1428
|
+
if (journal.$schema_version !== 1 || journal.self !== self
|
|
1429
|
+
|| (journal.state !== 'prepared' && journal.state !== 'committed')
|
|
1430
|
+
|| !Array.isArray(journal.changes))
|
|
1341
1431
|
throw new Error('Invalid Role mutation journal');
|
|
1432
|
+
const seen = new Set();
|
|
1342
1433
|
for (const change of journal.changes) {
|
|
1434
|
+
if (!change || typeof change !== 'object' || typeof change.file !== 'string'
|
|
1435
|
+
|| (change.before !== null && typeof change.before !== 'string')
|
|
1436
|
+
|| (change.after !== null && typeof change.after !== 'string')) {
|
|
1437
|
+
throw new Error('Role mutation journal contains an invalid change');
|
|
1438
|
+
}
|
|
1343
1439
|
const file = path.resolve(change.file);
|
|
1344
|
-
if (file
|
|
1440
|
+
if (file === root || !file.startsWith(root + path.sep))
|
|
1345
1441
|
throw new Error('Role mutation journal contains an unsafe path');
|
|
1442
|
+
if (seen.has(file))
|
|
1443
|
+
throw new Error('Role mutation journal contains duplicate file changes');
|
|
1444
|
+
seen.add(file);
|
|
1346
1445
|
}
|
|
1347
1446
|
}
|
|
1348
1447
|
function applyFileContent(file, content) {
|
|
@@ -1498,7 +1597,7 @@ function operationLabel(id) {
|
|
|
1498
1597
|
return labels[id] ?? title(id);
|
|
1499
1598
|
}
|
|
1500
1599
|
function validRoleOrNull(value, self) {
|
|
1501
|
-
return typeof value === 'string' &&
|
|
1600
|
+
return typeof value === 'string' && value !== 'owner' && getRoleDefinition(value, self) ? value : null;
|
|
1502
1601
|
}
|
|
1503
1602
|
function getNested(value, field) {
|
|
1504
1603
|
let cursor = value;
|
|
@@ -71,7 +71,7 @@ export function guardRoleCommand(content, activeChatType, isAdmin) {
|
|
|
71
71
|
const userGroupCommands = [
|
|
72
72
|
'/status', '/help', '/evolhelp', '/check', '/chatmode', '/mentionmode',
|
|
73
73
|
'/model', '/effort', '/baseagent', '/perm', '/activity', '/stop',
|
|
74
|
-
'/resume', '/trigger',
|
|
74
|
+
'/resume', '/trigger', '/file',
|
|
75
75
|
];
|
|
76
76
|
const userCommands = activeChatType === 'group' && !isAdmin
|
|
77
77
|
? userGroupCommands
|