ec-web 0.0.20 → 0.0.22
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/dist/ipc-client.js +7 -1
- package/dist/paths.js +18 -1
- package/dist/server.js +159 -28
- package/dist/sources/aid.js +69 -49
- package/dist/sources/baseagent-detector.js +60 -6
- package/dist/sources/basic-settings.js +818 -0
- package/dist/sources/gateway.js +5 -4
- package/dist/sources/monitor.js +22 -5
- package/dist/sources/msg.js +4 -96
- package/dist/sources/relation-roles.js +216 -207
- package/dist/sources/role-definitions.js +162 -331
- package/dist/sources/session-codex.js +207 -84
- package/dist/sources/system.js +10 -9
- package/dist/sources/triggers.js +82 -13
- package/dist/sources/types.js +20 -1
- package/dist/static/app.js +175 -15
- package/dist/static/basic-settings.js +1576 -0
- package/dist/static/index.html +43 -1
- package/dist/static/style.css +586 -2
- package/ecweb-wrapper.cjs +5 -1
- package/package.json +1 -1
|
@@ -14,23 +14,23 @@
|
|
|
14
14
|
import { getModelCatalogSnapshot } from './models.js';
|
|
15
15
|
import { resolveParentDistModule, toFileUrl } from './parent-package.js';
|
|
16
16
|
import { resolvePaths } from '../paths.js';
|
|
17
|
-
import
|
|
17
|
+
import crypto from 'node:crypto';
|
|
18
18
|
// 动态导入 evolcore 主项目的角色配置模块
|
|
19
19
|
async function getRolesModule() {
|
|
20
20
|
const rolesPath = resolveParentDistModule('config', 'roles.js');
|
|
21
21
|
const cmPath = resolveParentDistModule('config', 'config-manager.js');
|
|
22
22
|
const resolverPath = resolveParentDistModule('config', 'peer-role-resolver.js');
|
|
23
|
+
const roleServicePath = resolveParentDistModule('config', 'role-service.js');
|
|
23
24
|
const authPath = resolveParentDistModule('core', 'auth', 'auth-gateway.js');
|
|
24
25
|
// Windows 上需要转换为 file:// URL
|
|
25
26
|
try {
|
|
26
27
|
const rolesMod = await import(toFileUrl(rolesPath));
|
|
27
28
|
const cmMod = await import(toFileUrl(cmPath));
|
|
28
29
|
const resolverMod = await import(toFileUrl(resolverPath));
|
|
30
|
+
const roleServiceMod = await import(toFileUrl(roleServicePath));
|
|
29
31
|
const authMod = await import(toFileUrl(authPath));
|
|
30
32
|
if (!rolesMod.readRolesConfig)
|
|
31
33
|
throw new Error('readRolesConfig not found in roles.js');
|
|
32
|
-
if (!rolesMod.getBuiltinRolesConfig)
|
|
33
|
-
throw new Error('getBuiltinRolesConfig not found in roles.js');
|
|
34
34
|
if (!rolesMod.roleExists)
|
|
35
35
|
throw new Error('roleExists not found in roles.js');
|
|
36
36
|
if (!cmMod.read)
|
|
@@ -43,9 +43,13 @@ async function getRolesModule() {
|
|
|
43
43
|
throw new Error('buildAuthSubject not found in auth-gateway.js');
|
|
44
44
|
if (!authMod.authorizeOperation)
|
|
45
45
|
throw new Error('authorizeOperation not found in auth-gateway.js');
|
|
46
|
+
if (!roleServiceMod.readRoleRegistry || !roleServiceMod.writeRoleRegistry
|
|
47
|
+
|| !roleServiceMod.readRoleDefinition || !roleServiceMod.writeRoleDefinition
|
|
48
|
+
|| !roleServiceMod.registerRole || !roleServiceMod.unregisterRole
|
|
49
|
+
|| !roleServiceMod.resetRoleDefinition)
|
|
50
|
+
throw new Error('role-service exports are incomplete');
|
|
46
51
|
return {
|
|
47
52
|
readRolesConfig: rolesMod.readRolesConfig,
|
|
48
|
-
getBuiltinRolesConfig: rolesMod.getBuiltinRolesConfig,
|
|
49
53
|
roleExists: rolesMod.roleExists,
|
|
50
54
|
read: cmMod.read,
|
|
51
55
|
write: cmMod.write,
|
|
@@ -53,6 +57,7 @@ async function getRolesModule() {
|
|
|
53
57
|
resolver: resolverMod,
|
|
54
58
|
buildAuthSubject: authMod.buildAuthSubject,
|
|
55
59
|
authorizeOperation: authMod.authorizeOperation,
|
|
60
|
+
roleService: roleServiceMod,
|
|
56
61
|
};
|
|
57
62
|
}
|
|
58
63
|
catch (err) {
|
|
@@ -78,78 +83,19 @@ async function getOperationRegistryModule() {
|
|
|
78
83
|
listOperations: mod.listOperations,
|
|
79
84
|
};
|
|
80
85
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
throw new Error(response?.error || 'Role Menu is unavailable');
|
|
89
|
-
return response.response;
|
|
90
|
-
}
|
|
91
|
-
function roleMenuHttpStatus(code) {
|
|
92
|
-
if (code === 'NOT_FOUND')
|
|
93
|
-
return 404;
|
|
94
|
-
if (code === 'CONFLICT')
|
|
95
|
-
return 409;
|
|
96
|
-
if (code === 'PERMISSION_DENIED' || code === 'NOT_ALLOWED')
|
|
97
|
-
return 403;
|
|
98
|
-
if (code === 'TEMPORARILY_UNAVAILABLE')
|
|
99
|
-
return 503;
|
|
100
|
-
return 400;
|
|
101
|
-
}
|
|
102
|
-
function sendRoleMenuError(res, response) {
|
|
103
|
-
const error = response?.error ?? { code: 'INTERNAL_ERROR', message: 'Role Menu request failed' };
|
|
104
|
-
sendJson(res, roleMenuHttpStatus(error.code), {
|
|
105
|
-
error: error.message,
|
|
106
|
-
code: error.code,
|
|
107
|
-
data: error.data,
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
async function rolePolicyRevision(aid, auth) {
|
|
111
|
-
const response = await execRoleMenu(aid, {
|
|
112
|
-
type: 'menu.query', id: `ecweb-role-revision-${Date.now()}`,
|
|
113
|
-
args: { view: 'defaults' },
|
|
114
|
-
}, auth);
|
|
115
|
-
if (response.error)
|
|
116
|
-
throw Object.assign(new Error(response.error.message), { menuResponse: response });
|
|
117
|
-
return response.data.policyRevision;
|
|
86
|
+
function ecwebAuditContext(auth, aid, operation) {
|
|
87
|
+
return {
|
|
88
|
+
ecwebRequestId: auth.ecwebRequestId ?? crypto.randomUUID(),
|
|
89
|
+
targetAgentAid: aid,
|
|
90
|
+
endpoint: auth.endpoint ?? 'ECWeb role policy',
|
|
91
|
+
logicalToolName: operation,
|
|
92
|
+
};
|
|
118
93
|
}
|
|
119
94
|
export function visibleRoleDefinitionOptions(roleOptions) {
|
|
120
95
|
return roleOptions
|
|
121
96
|
.filter(item => item?.kind === 'user' || item?.value === 'admin')
|
|
122
97
|
.sort((left, right) => Number(right.value === 'admin') - Number(left.value === 'admin'));
|
|
123
98
|
}
|
|
124
|
-
async function buildMenuSnapshot(aid, agents, auth) {
|
|
125
|
-
const [roleOptions, defaults] = await Promise.all([
|
|
126
|
-
execRoleMenu(aid, { type: 'menu.options', id: `ecweb-role-options-${Date.now()}`, args: { option: 'roles' } }, auth),
|
|
127
|
-
execRoleMenu(aid, { type: 'menu.query', id: `ecweb-role-defaults-${Date.now()}`, args: { view: 'defaults' } }, auth),
|
|
128
|
-
]);
|
|
129
|
-
if (roleOptions.error)
|
|
130
|
-
throw Object.assign(new Error(roleOptions.error.message), { menuResponse: roleOptions });
|
|
131
|
-
if (defaults.error)
|
|
132
|
-
throw Object.assign(new Error(defaults.error.message), { menuResponse: defaults });
|
|
133
|
-
const visibleRoles = visibleRoleDefinitionOptions(roleOptions.data);
|
|
134
|
-
const details = await Promise.all(visibleRoles.map(item => execRoleMenu(aid, {
|
|
135
|
-
type: 'menu.query', id: `ecweb-role-${item.value}-${Date.now()}`,
|
|
136
|
-
args: { view: 'role', roleId: item.value },
|
|
137
|
-
}, auth)));
|
|
138
|
-
const roles = {};
|
|
139
|
-
for (const detail of details) {
|
|
140
|
-
if (!detail.error)
|
|
141
|
-
roles[detail.data.roleId] = detail.data.definition;
|
|
142
|
-
}
|
|
143
|
-
return {
|
|
144
|
-
$schema_version: 1,
|
|
145
|
-
aid,
|
|
146
|
-
self: aid,
|
|
147
|
-
agents: summarizeAgents(agents),
|
|
148
|
-
defaultRoles: defaults.data.defaultRoles,
|
|
149
|
-
policyRevision: defaults.data.policyRevision,
|
|
150
|
-
roles,
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
99
|
function isWriteMethod(method) {
|
|
154
100
|
return method === 'PUT' || method === 'POST' || method === 'DELETE';
|
|
155
101
|
}
|
|
@@ -160,9 +106,10 @@ async function authorizeRolePolicyOperation(modules, processConfig, auth, aid, o
|
|
|
160
106
|
const actor = auth.actorAid || '';
|
|
161
107
|
const processOwners = Array.isArray(processConfig?.owners) ? processConfig.owners.map(String) : [];
|
|
162
108
|
const syntheticOwner = !!auth.localDirect || (!!actor && processOwners.includes(actor));
|
|
109
|
+
const actorId = auth.localDirect ? 'local-direct' : actor || undefined;
|
|
163
110
|
const subject = modules.buildAuthSubject({
|
|
164
111
|
selfAid: aid,
|
|
165
|
-
actorId
|
|
112
|
+
actorId,
|
|
166
113
|
channel: `ecweb#${aid}`,
|
|
167
114
|
channelType: 'aun',
|
|
168
115
|
channelId: actor || 'ecweb',
|
|
@@ -176,6 +123,19 @@ async function authorizeRolePolicyOperation(modules, processConfig, auth, aid, o
|
|
|
176
123
|
isAuthenticated: true,
|
|
177
124
|
allowAccess: true,
|
|
178
125
|
roleExists: true,
|
|
126
|
+
// buildAuthSubject uses the authenticated actor separately from the
|
|
127
|
+
// resolved relation role to determine daemon-owner provenance.
|
|
128
|
+
actor: {
|
|
129
|
+
operatorId: actorId || 'ecweb',
|
|
130
|
+
operatorChannelKey: `ecweb#${aid}`,
|
|
131
|
+
operatorChannelType: 'aun',
|
|
132
|
+
...(actor ? {
|
|
133
|
+
principalId: actor,
|
|
134
|
+
principalStatus: 'native_aid',
|
|
135
|
+
} : {
|
|
136
|
+
principalStatus: 'unmapped',
|
|
137
|
+
}),
|
|
138
|
+
},
|
|
179
139
|
},
|
|
180
140
|
} : {}),
|
|
181
141
|
});
|
|
@@ -191,8 +151,18 @@ async function authorizeRolePolicyOperation(modules, processConfig, auth, aid, o
|
|
|
191
151
|
...args,
|
|
192
152
|
},
|
|
193
153
|
},
|
|
154
|
+
auditMetadata: ecwebAuditContext(auth, aid, operation),
|
|
194
155
|
});
|
|
195
156
|
}
|
|
157
|
+
function isDaemonOwnerAuth(processConfig, auth) {
|
|
158
|
+
if (auth.localDirect)
|
|
159
|
+
return true;
|
|
160
|
+
const actor = typeof auth.actorAid === 'string' ? auth.actorAid.trim() : '';
|
|
161
|
+
const processOwners = Array.isArray(processConfig?.owners)
|
|
162
|
+
? processConfig.owners.map((value) => String(value || '').trim()).filter(Boolean)
|
|
163
|
+
: [];
|
|
164
|
+
return !!actor && processOwners.includes(actor);
|
|
165
|
+
}
|
|
196
166
|
const MODEL_PERMISSION_FIELD = 'baseagents.claude.model';
|
|
197
167
|
const MODEL_PATTERN_OPTIONS = ['*', 'claude-opus-*', 'claude-sonnet-*', 'claude-haiku-*'];
|
|
198
168
|
const COMMAND_PERMISSION_SCOPES = new Set(['relation', 'role', 'agent', 'process', 'filesystem', 'control', 'raw-cli']);
|
|
@@ -313,33 +283,24 @@ function isReservedRoleName(roleName) {
|
|
|
313
283
|
function isValidUserRoleName(roleName) {
|
|
314
284
|
return /^[a-z0-9_-]+$/.test(roleName) && !isReservedRoleName(roleName);
|
|
315
285
|
}
|
|
316
|
-
function
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
}
|
|
325
|
-
function getAgentRoleDefinitions(config) {
|
|
326
|
-
return config.roles?.definitions && typeof config.roles.definitions === 'object'
|
|
327
|
-
? config.roles.definitions
|
|
328
|
-
: {};
|
|
286
|
+
function writeRoleRegistry(modules, aid, defaultRoles) {
|
|
287
|
+
const registry = modules.roleService.readRoleRegistry(aid);
|
|
288
|
+
modules.roleService.writeRoleRegistry(aid, {
|
|
289
|
+
...registry,
|
|
290
|
+
defaultRoles: {
|
|
291
|
+
private: defaultRoles.private ?? null,
|
|
292
|
+
group: defaultRoles.group ?? null,
|
|
293
|
+
},
|
|
294
|
+
});
|
|
329
295
|
}
|
|
330
|
-
function
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
roles.definitions = { ...roles.definitions };
|
|
296
|
+
function writeRoleDefinition(modules, aid, roleName, definition) {
|
|
297
|
+
const registry = modules.roleService.readRoleRegistry(aid);
|
|
298
|
+
if (!registry.roles.includes(roleName)) {
|
|
299
|
+
modules.roleService.registerRole(aid, roleName, definition);
|
|
335
300
|
}
|
|
336
|
-
|
|
337
|
-
|
|
301
|
+
else {
|
|
302
|
+
modules.roleService.writeRoleDefinition(aid, roleName, definition);
|
|
338
303
|
}
|
|
339
|
-
updater(roles, config);
|
|
340
|
-
const next = { ...config, roles };
|
|
341
|
-
writeAgentConfig(modules, aid, next);
|
|
342
|
-
return next;
|
|
343
304
|
}
|
|
344
305
|
function normalizeDefaultRoleInput(value, current) {
|
|
345
306
|
if (value === null || value === '')
|
|
@@ -623,11 +584,42 @@ async function buildModelPermissionResponse(roleName, roleDef, override) {
|
|
|
623
584
|
}
|
|
624
585
|
async function buildSnapshot(params = {}, auth = {}) {
|
|
625
586
|
try {
|
|
587
|
+
auth = {
|
|
588
|
+
...auth,
|
|
589
|
+
ecwebRequestId: params.__ecwebRequestId ?? auth.ecwebRequestId ?? crypto.randomUUID(),
|
|
590
|
+
endpoint: params.__ecwebEndpoint ?? auth.endpoint ?? 'WS snapshot:roleDefinitions',
|
|
591
|
+
};
|
|
626
592
|
const agents = await getAgentsFromIpc();
|
|
627
593
|
const aid = resolveRequestedAid(params.aid, agents);
|
|
628
594
|
if (!aid)
|
|
629
595
|
return { $schema_version: 1, aid: null, agents: [], roles: {}, defaultRoles: { private: null, group: null } };
|
|
630
|
-
|
|
596
|
+
const modules = await getRolesModule();
|
|
597
|
+
const processConfig = modules.read(modules.ConfigTarget.Process) || {};
|
|
598
|
+
const decision = await authorizeRolePolicyOperation(modules, processConfig, auth, aid, 'role.policy.read', {
|
|
599
|
+
method: 'GET',
|
|
600
|
+
path: '/api/role-definitions',
|
|
601
|
+
});
|
|
602
|
+
if (!decision.allow) {
|
|
603
|
+
return {
|
|
604
|
+
$schema_version: 1,
|
|
605
|
+
aid,
|
|
606
|
+
agents: summarizeAgents(agents),
|
|
607
|
+
roles: {},
|
|
608
|
+
defaultRoles: { private: null, group: null },
|
|
609
|
+
error: decision.reason || 'forbidden: role policy read not allowed',
|
|
610
|
+
code: decision.code,
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
const config = modules.readRolesConfig(aid);
|
|
614
|
+
return {
|
|
615
|
+
$schema_version: 1,
|
|
616
|
+
aid,
|
|
617
|
+
self: aid,
|
|
618
|
+
agents: summarizeAgents(agents),
|
|
619
|
+
defaultRoles: config.defaultRoles ?? { private: null, group: null },
|
|
620
|
+
policyRevision: JSON.stringify(config),
|
|
621
|
+
roles: config.roles ?? {},
|
|
622
|
+
};
|
|
631
623
|
}
|
|
632
624
|
catch (err) {
|
|
633
625
|
console.error('[role-definitions] Failed to build snapshot:', err);
|
|
@@ -637,204 +629,30 @@ async function buildSnapshot(params = {}, auth = {}) {
|
|
|
637
629
|
export const roleDefinitionsSource = {
|
|
638
630
|
kind: 'roleDefinitions',
|
|
639
631
|
async snapshot(params = {}) {
|
|
640
|
-
return buildSnapshot(params,
|
|
632
|
+
return buildSnapshot(params, {
|
|
633
|
+
...(params.__menuAuth ?? {}),
|
|
634
|
+
ecwebRequestId: params.__ecwebRequestId,
|
|
635
|
+
endpoint: params.__ecwebEndpoint,
|
|
636
|
+
});
|
|
641
637
|
},
|
|
642
638
|
subscribe(_params, _push) {
|
|
643
639
|
return () => { };
|
|
644
640
|
}
|
|
645
641
|
};
|
|
646
|
-
async function handleRoleMenuCompatibility(req, res, auth, aid, agents, parsedUrl) {
|
|
647
|
-
const urlPath = parsedUrl.path;
|
|
648
|
-
const roleRoute = urlPath.match(/^\/api\/role-definitions\/([^/]+)$/);
|
|
649
|
-
const roleNestedRoute = urlPath.match(/^\/api\/role-definitions\/([^/]+)\/([^/]+)$/);
|
|
650
|
-
const decodeRole = (raw) => {
|
|
651
|
-
try {
|
|
652
|
-
return decodeURIComponent(raw);
|
|
653
|
-
}
|
|
654
|
-
catch {
|
|
655
|
-
return raw;
|
|
656
|
-
}
|
|
657
|
-
};
|
|
658
|
-
const requireAid = () => {
|
|
659
|
-
if (aid)
|
|
660
|
-
return true;
|
|
661
|
-
sendJson(res, 400, { error: 'aid is required' });
|
|
662
|
-
return false;
|
|
663
|
-
};
|
|
664
|
-
const execute = async (payload, successStatus = 200) => {
|
|
665
|
-
const response = await execRoleMenu(aid, payload, auth);
|
|
666
|
-
if (response.error) {
|
|
667
|
-
sendRoleMenuError(res, response);
|
|
668
|
-
return null;
|
|
669
|
-
}
|
|
670
|
-
if (successStatus > 0)
|
|
671
|
-
sendJson(res, successStatus, response.data);
|
|
672
|
-
return response.data;
|
|
673
|
-
};
|
|
674
|
-
try {
|
|
675
|
-
if (req.method === 'GET' && urlPath === '/api/role-definitions') {
|
|
676
|
-
if (!requireAid())
|
|
677
|
-
return true;
|
|
678
|
-
try {
|
|
679
|
-
sendJson(res, 200, await buildMenuSnapshot(aid, agents, auth));
|
|
680
|
-
}
|
|
681
|
-
catch (error) {
|
|
682
|
-
if (error?.menuResponse)
|
|
683
|
-
sendRoleMenuError(res, error.menuResponse);
|
|
684
|
-
else
|
|
685
|
-
throw error;
|
|
686
|
-
}
|
|
687
|
-
return true;
|
|
688
|
-
}
|
|
689
|
-
if (req.method === 'GET' && urlPath === '/api/role-definitions/operations') {
|
|
690
|
-
if (!requireAid())
|
|
691
|
-
return true;
|
|
692
|
-
const data = await execute({
|
|
693
|
-
type: 'menu.options', id: `ecweb-role-operations-${Date.now()}`, args: { option: 'operations' },
|
|
694
|
-
}, 0);
|
|
695
|
-
if (data)
|
|
696
|
-
sendJson(res, 200, { operations: data.map((item) => ({ id: item.value, ...item })) });
|
|
697
|
-
return true;
|
|
698
|
-
}
|
|
699
|
-
if (req.method === 'PUT' && urlPath === '/api/role-definitions') {
|
|
700
|
-
if (!requireAid())
|
|
701
|
-
return true;
|
|
702
|
-
const body = await readJsonBody(req);
|
|
703
|
-
if (body.roles !== undefined) {
|
|
704
|
-
sendJson(res, 400, { error: 'Bulk role replacement is not supported; update roles individually' });
|
|
705
|
-
return true;
|
|
706
|
-
}
|
|
707
|
-
const revision = await rolePolicyRevision(aid, auth);
|
|
708
|
-
const data = await execute({
|
|
709
|
-
type: 'menu.update', id: `ecweb-role-defaults-update-${Date.now()}`,
|
|
710
|
-
args: { resource: 'defaultRoles', expectedPolicyRevision: revision },
|
|
711
|
-
value: JSON.stringify(body.defaultRoles ?? {}),
|
|
712
|
-
}, 0);
|
|
713
|
-
if (data)
|
|
714
|
-
sendJson(res, 200, { ok: true, data: await buildMenuSnapshot(aid, agents, auth) });
|
|
715
|
-
return true;
|
|
716
|
-
}
|
|
717
|
-
if (req.method === 'POST' && urlPath === '/api/role-definitions') {
|
|
718
|
-
if (!requireAid())
|
|
719
|
-
return true;
|
|
720
|
-
const body = await readJsonBody(req);
|
|
721
|
-
const { name, ...definition } = body;
|
|
722
|
-
const revision = await rolePolicyRevision(aid, auth);
|
|
723
|
-
const data = await execute({
|
|
724
|
-
type: 'menu.action', id: `ecweb-role-create-${Date.now()}`, action: 'create',
|
|
725
|
-
args: { roleId: name, definition, expectedPolicyRevision: revision },
|
|
726
|
-
}, 0);
|
|
727
|
-
if (data)
|
|
728
|
-
sendJson(res, 201, { ok: true, role: data.definition, policyRevision: data.policyRevision });
|
|
729
|
-
return true;
|
|
730
|
-
}
|
|
731
|
-
if (req.method === 'GET' && roleRoute) {
|
|
732
|
-
if (!requireAid())
|
|
733
|
-
return true;
|
|
734
|
-
const data = await execute({
|
|
735
|
-
type: 'menu.query', id: `ecweb-role-detail-${Date.now()}`,
|
|
736
|
-
args: { view: 'role', roleId: decodeRole(roleRoute[1]) },
|
|
737
|
-
}, 0);
|
|
738
|
-
if (data)
|
|
739
|
-
sendJson(res, 200, data.definition);
|
|
740
|
-
return true;
|
|
741
|
-
}
|
|
742
|
-
if (req.method === 'PUT' && roleRoute) {
|
|
743
|
-
if (!requireAid())
|
|
744
|
-
return true;
|
|
745
|
-
const updates = await readJsonBody(req);
|
|
746
|
-
const revision = await rolePolicyRevision(aid, auth);
|
|
747
|
-
const data = await execute({
|
|
748
|
-
type: 'menu.update', id: `ecweb-role-update-${Date.now()}`,
|
|
749
|
-
args: { resource: 'definition', roleId: decodeRole(roleRoute[1]), expectedPolicyRevision: revision },
|
|
750
|
-
value: JSON.stringify(updates),
|
|
751
|
-
}, 0);
|
|
752
|
-
if (data)
|
|
753
|
-
sendJson(res, 200, { ok: true, policyRevision: data.policyRevision });
|
|
754
|
-
return true;
|
|
755
|
-
}
|
|
756
|
-
if (req.method === 'POST' && roleNestedRoute?.[2] === 'reset') {
|
|
757
|
-
if (!requireAid())
|
|
758
|
-
return true;
|
|
759
|
-
const revision = await rolePolicyRevision(aid, auth);
|
|
760
|
-
const data = await execute({
|
|
761
|
-
type: 'menu.action', id: `ecweb-role-reset-${Date.now()}`, action: 'reset',
|
|
762
|
-
args: { roleId: decodeRole(roleNestedRoute[1]), expectedPolicyRevision: revision },
|
|
763
|
-
}, 0);
|
|
764
|
-
if (data) {
|
|
765
|
-
const detail = await execRoleMenu(aid, {
|
|
766
|
-
type: 'menu.query', id: `ecweb-role-reset-detail-${Date.now()}`,
|
|
767
|
-
args: { view: 'role', roleId: decodeRole(roleNestedRoute[1]) },
|
|
768
|
-
}, auth);
|
|
769
|
-
sendJson(res, 200, { ok: true, role: detail.data?.definition, policyRevision: data.policyRevision });
|
|
770
|
-
}
|
|
771
|
-
return true;
|
|
772
|
-
}
|
|
773
|
-
if (req.method === 'DELETE' && roleRoute) {
|
|
774
|
-
if (!requireAid())
|
|
775
|
-
return true;
|
|
776
|
-
const revision = await rolePolicyRevision(aid, auth);
|
|
777
|
-
const actionArgs = {
|
|
778
|
-
roleId: decodeRole(roleRoute[1]),
|
|
779
|
-
expectedPolicyRevision: revision,
|
|
780
|
-
};
|
|
781
|
-
if (Object.prototype.hasOwnProperty.call(parsedUrl.query, 'replacementRoleId')) {
|
|
782
|
-
actionArgs.replacementRoleId = parsedUrl.query.replacementRoleId || null;
|
|
783
|
-
}
|
|
784
|
-
else if (parsedUrl.query.clearReferences === 'true') {
|
|
785
|
-
actionArgs.replacementRoleId = null;
|
|
786
|
-
}
|
|
787
|
-
const data = await execute({
|
|
788
|
-
type: 'menu.action', id: `ecweb-role-delete-${Date.now()}`, action: 'delete', args: actionArgs,
|
|
789
|
-
}, 0);
|
|
790
|
-
if (data)
|
|
791
|
-
sendJson(res, 200, { ok: true, ...data });
|
|
792
|
-
return true;
|
|
793
|
-
}
|
|
794
|
-
if (req.method === 'PUT' && roleNestedRoute?.[2] === 'model-permissions') {
|
|
795
|
-
if (!requireAid())
|
|
796
|
-
return true;
|
|
797
|
-
const payload = await readJsonBody(req);
|
|
798
|
-
const revision = await rolePolicyRevision(aid, auth);
|
|
799
|
-
const permission = {
|
|
800
|
-
default: payload.defaultModel ?? payload.default,
|
|
801
|
-
allowOverride: payload.allowOverride === true,
|
|
802
|
-
allowedModels: Array.isArray(payload.allowedModels) ? payload.allowedModels : [],
|
|
803
|
-
};
|
|
804
|
-
const data = await execute({
|
|
805
|
-
type: 'menu.update', id: `ecweb-role-model-update-${Date.now()}`,
|
|
806
|
-
args: { resource: 'definition', roleId: decodeRole(roleNestedRoute[1]), expectedPolicyRevision: revision },
|
|
807
|
-
value: JSON.stringify({ permissions: { [MODEL_PERMISSION_FIELD]: permission } }),
|
|
808
|
-
}, 0);
|
|
809
|
-
if (data)
|
|
810
|
-
sendJson(res, 200, { success: true, data: await buildModelPermissionResponse(decodeRole(roleNestedRoute[1]), data.definition) });
|
|
811
|
-
return true;
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
catch (error) {
|
|
815
|
-
if (error?.menuResponse)
|
|
816
|
-
sendRoleMenuError(res, error.menuResponse);
|
|
817
|
-
else
|
|
818
|
-
sendJson(res, 500, { error: error?.message || String(error) });
|
|
819
|
-
return true;
|
|
820
|
-
}
|
|
821
|
-
return false;
|
|
822
|
-
}
|
|
823
642
|
// HTTP API handler
|
|
824
643
|
export async function handleRoleDefinitionsApi(req, res, auth = {}) {
|
|
825
644
|
try {
|
|
826
645
|
const parsedUrl = parseUrl(req.url || '');
|
|
827
646
|
const urlPath = parsedUrl.path;
|
|
647
|
+
auth = {
|
|
648
|
+
...auth,
|
|
649
|
+
ecwebRequestId: auth.ecwebRequestId ?? crypto.randomUUID(),
|
|
650
|
+
endpoint: auth.endpoint ?? `${req.method ?? 'UNKNOWN'} ${urlPath}`,
|
|
651
|
+
};
|
|
828
652
|
const agents = await getAgentsFromIpc();
|
|
829
653
|
const aid = resolveRequestedAid(parsedUrl.query.aid, agents);
|
|
830
|
-
if (await handleRoleMenuCompatibility(req, res, auth, aid, agents, parsedUrl))
|
|
831
|
-
return;
|
|
832
|
-
if (isWriteMethod(req.method)) {
|
|
833
|
-
sendJson(res, 405, { error: 'This Role endpoint is not supported; use Role Menu' });
|
|
834
|
-
return;
|
|
835
|
-
}
|
|
836
654
|
const modules = await getRolesModule();
|
|
837
|
-
const { readRolesConfig,
|
|
655
|
+
const { readRolesConfig, read, ConfigTarget } = modules;
|
|
838
656
|
const roleRoute = urlPath.match(/^\/api\/role-definitions\/([^/]+)$/);
|
|
839
657
|
const roleNestedRoute = urlPath.match(/^\/api\/role-definitions\/([^/]+)\/([^/]+)$/);
|
|
840
658
|
const decodeRoleName = (raw) => {
|
|
@@ -881,9 +699,25 @@ export async function handleRoleDefinitionsApi(req, res, auth = {}) {
|
|
|
881
699
|
});
|
|
882
700
|
return;
|
|
883
701
|
}
|
|
702
|
+
if (isWriteMethod(req.method) && !isDaemonOwnerAuth(processConfig, auth)
|
|
703
|
+
&& decision.subject?.role === 'admin') {
|
|
704
|
+
const targetedRole = roleNestedRoute
|
|
705
|
+
? decodeRoleName(roleNestedRoute[1])
|
|
706
|
+
: roleRoute
|
|
707
|
+
? decodeRoleName(roleRoute[1])
|
|
708
|
+
: null;
|
|
709
|
+
if (targetedRole === 'owner') {
|
|
710
|
+
sendJson(res, 403, {
|
|
711
|
+
error: 'forbidden: agent admin cannot modify owner role policy',
|
|
712
|
+
code: 'PERMISSION_DENIED',
|
|
713
|
+
actorRole: 'admin',
|
|
714
|
+
});
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
884
718
|
}
|
|
885
719
|
if (req.method === 'GET' && urlPath === '/api/role-definitions') {
|
|
886
|
-
sendJson(res, 200, await buildSnapshot({ aid }));
|
|
720
|
+
sendJson(res, 200, await buildSnapshot({ aid, __ecwebRequestId: auth.ecwebRequestId, __ecwebEndpoint: auth.endpoint }, auth));
|
|
887
721
|
return;
|
|
888
722
|
}
|
|
889
723
|
if (req.method === 'PUT' && urlPath === '/api/role-definitions') {
|
|
@@ -914,22 +748,38 @@ export async function handleRoleDefinitionsApi(req, res, auth = {}) {
|
|
|
914
748
|
}
|
|
915
749
|
}
|
|
916
750
|
}
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
private: normalizeDefaultRoleInput(incoming.defaultRoles.private, currentConfig.defaultRoles?.private),
|
|
921
|
-
group: normalizeDefaultRoleInput(incoming.defaultRoles.group, currentConfig.defaultRoles?.group),
|
|
922
|
-
};
|
|
751
|
+
if (incoming.roles && typeof incoming.roles === 'object') {
|
|
752
|
+
for (const [roleName, roleDef] of Object.entries(incoming.roles)) {
|
|
753
|
+
writeRoleDefinition(modules, aid, roleName, normalizeRoleDefinitionForSave(roleDef));
|
|
923
754
|
}
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
755
|
+
}
|
|
756
|
+
if (incoming.defaultRoles && typeof incoming.defaultRoles === 'object') {
|
|
757
|
+
const nextDefaults = {
|
|
758
|
+
private: normalizeDefaultRoleInput(incoming.defaultRoles.private, currentConfig.defaultRoles?.private),
|
|
759
|
+
group: normalizeDefaultRoleInput(incoming.defaultRoles.group, currentConfig.defaultRoles?.group),
|
|
760
|
+
};
|
|
761
|
+
const processConfig = modules.read(modules.ConfigTarget.Process) || {};
|
|
762
|
+
const daemonOwner = isDaemonOwnerAuth(processConfig, auth);
|
|
763
|
+
const actorRole = daemonOwner
|
|
764
|
+
? 'owner'
|
|
765
|
+
: auth.actorAid
|
|
766
|
+
? (modules.resolver.resolvePeerRoleDetail({
|
|
767
|
+
selfAid: aid,
|
|
768
|
+
channelType: 'aun',
|
|
769
|
+
chatType: 'private',
|
|
770
|
+
actorId: auth.actorAid,
|
|
771
|
+
conversationId: auth.actorAid,
|
|
772
|
+
}).effectiveRole || 'none')
|
|
773
|
+
: 'none';
|
|
774
|
+
const changesAdminDefault = ['private', 'group'].some(scene => currentConfig.defaultRoles?.[scene] !== nextDefaults[scene]
|
|
775
|
+
&& (currentConfig.defaultRoles?.[scene] === 'admin' || nextDefaults[scene] === 'admin'));
|
|
776
|
+
if (!daemonOwner && actorRole !== 'owner' && changesAdminDefault) {
|
|
777
|
+
sendJson(res, 403, { error: 'Only an owner can set or replace an admin default role' });
|
|
778
|
+
return;
|
|
930
779
|
}
|
|
931
|
-
|
|
932
|
-
|
|
780
|
+
writeRoleRegistry(modules, aid, nextDefaults);
|
|
781
|
+
}
|
|
782
|
+
sendJson(res, 200, { ok: true, data: await buildSnapshot({ aid, __ecwebRequestId: auth.ecwebRequestId, __ecwebEndpoint: auth.endpoint }, auth) });
|
|
933
783
|
}
|
|
934
784
|
catch (err) {
|
|
935
785
|
console.error('[role-definitions] Failed to update agent role config:', err);
|
|
@@ -975,9 +825,7 @@ export async function handleRoleDefinitionsApi(req, res, auth = {}) {
|
|
|
975
825
|
if (usageLimits !== undefined) {
|
|
976
826
|
newRole.usageLimits = normalizeRoleUsageLimits(usageLimits);
|
|
977
827
|
}
|
|
978
|
-
|
|
979
|
-
roles.definitions = { ...(roles.definitions || {}), [name]: newRole };
|
|
980
|
-
});
|
|
828
|
+
modules.roleService.registerRole(aid, name, newRole);
|
|
981
829
|
sendJson(res, 201, { ok: true, role: newRole });
|
|
982
830
|
}
|
|
983
831
|
catch (err) {
|
|
@@ -1042,30 +890,19 @@ export async function handleRoleDefinitionsApi(req, res, auth = {}) {
|
|
|
1042
890
|
allowOverride: validation.value.allowOverride,
|
|
1043
891
|
allowedModels: validation.value.allowedModels,
|
|
1044
892
|
};
|
|
1045
|
-
|
|
1046
|
-
roles.definitions = { ...(roles.definitions || {}), [roleName]: roleDef };
|
|
1047
|
-
});
|
|
893
|
+
modules.roleService.writeRoleDefinition(aid, roleName, roleDef);
|
|
1048
894
|
const data = await buildModelPermissionResponse(roleName, roleDef, validation.value);
|
|
1049
895
|
sendJson(res, 200, { success: true, data });
|
|
1050
896
|
return;
|
|
1051
897
|
}
|
|
1052
898
|
if (req.method === 'POST' && roleNestedRoute?.[2] === 'reset') {
|
|
1053
899
|
const roleName = decodeRoleName(roleNestedRoute[1]);
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
if (!builtinRole) {
|
|
1057
|
-
sendJson(res, 404, { error: 'Role not found in builtin config' });
|
|
900
|
+
if (!['admin', 'member', 'visitor'].includes(roleName)) {
|
|
901
|
+
sendJson(res, 403, { error: 'Only builtin role overrides can be reset' });
|
|
1058
902
|
return;
|
|
1059
903
|
}
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
delete definitions[roleName];
|
|
1063
|
-
if (Object.keys(definitions).length)
|
|
1064
|
-
roles.definitions = definitions;
|
|
1065
|
-
else
|
|
1066
|
-
delete roles.definitions;
|
|
1067
|
-
});
|
|
1068
|
-
sendJson(res, 200, { ok: true, role: builtinRole });
|
|
904
|
+
modules.roleService.resetRoleDefinition(aid, roleName);
|
|
905
|
+
sendJson(res, 200, { ok: true, role: readRolesConfig(aid).roles[roleName] });
|
|
1069
906
|
return;
|
|
1070
907
|
}
|
|
1071
908
|
if (req.method === 'GET' && roleRoute) {
|
|
@@ -1081,25 +918,21 @@ export async function handleRoleDefinitionsApi(req, res, auth = {}) {
|
|
|
1081
918
|
}
|
|
1082
919
|
if (req.method === 'DELETE' && roleRoute) {
|
|
1083
920
|
const roleName = decodeRoleName(roleRoute[1]);
|
|
1084
|
-
|
|
1085
|
-
if (builtinRoles.has(roleName) || isReservedRoleName(roleName)) {
|
|
921
|
+
if (['owner', 'admin', 'member', 'visitor'].includes(roleName)) {
|
|
1086
922
|
sendJson(res, 403, { error: 'Cannot delete builtin or management role' });
|
|
1087
923
|
return;
|
|
1088
924
|
}
|
|
1089
|
-
const
|
|
1090
|
-
|
|
1091
|
-
if (!definitions[roleName]) {
|
|
925
|
+
const config = readRolesConfig(aid);
|
|
926
|
+
if (!config.roles[roleName]) {
|
|
1092
927
|
sendJson(res, 404, { error: 'Role not found' });
|
|
1093
928
|
return;
|
|
1094
929
|
}
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
delete roles.definitions;
|
|
1102
|
-
});
|
|
930
|
+
const replacement = Object.prototype.hasOwnProperty.call(parsedUrl.query, 'replacementRoleId')
|
|
931
|
+
? (parsedUrl.query.replacementRoleId || null)
|
|
932
|
+
: parsedUrl.query.clearReferences === 'true'
|
|
933
|
+
? null
|
|
934
|
+
: undefined;
|
|
935
|
+
modules.roleService.unregisterRole(aid, roleName, replacement);
|
|
1103
936
|
sendJson(res, 200, { ok: true });
|
|
1104
937
|
return;
|
|
1105
938
|
}
|
|
@@ -1157,9 +990,7 @@ export async function handleRoleDefinitionsApi(req, res, auth = {}) {
|
|
|
1157
990
|
sendJson(res, 400, { error: 'Invalid model permissions', errors: validation.errors });
|
|
1158
991
|
return;
|
|
1159
992
|
}
|
|
1160
|
-
|
|
1161
|
-
roles.definitions = { ...(roles.definitions || {}), [roleName]: nextRole };
|
|
1162
|
-
});
|
|
993
|
+
modules.roleService.writeRoleDefinition(aid, roleName, nextRole);
|
|
1163
994
|
sendJson(res, 200, { ok: true });
|
|
1164
995
|
}
|
|
1165
996
|
catch (err) {
|