ec-web 0.0.20 → 0.0.21
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/server.js +117 -23
- 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/msg.js +4 -96
- package/dist/sources/relation-roles.js +150 -191
- package/dist/sources/role-definitions.js +138 -331
- package/dist/sources/session-codex.js +189 -78
- package/dist/sources/system.js +10 -9
- package/dist/sources/triggers.js +24 -13
- package/dist/sources/types.js +20 -1
- package/dist/static/app.js +83 -9
- 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,22 @@
|
|
|
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 { ipcQuery } from '../ipc-client.js';
|
|
18
17
|
// 动态导入 evolcore 主项目的角色配置模块
|
|
19
18
|
async function getRolesModule() {
|
|
20
19
|
const rolesPath = resolveParentDistModule('config', 'roles.js');
|
|
21
20
|
const cmPath = resolveParentDistModule('config', 'config-manager.js');
|
|
22
21
|
const resolverPath = resolveParentDistModule('config', 'peer-role-resolver.js');
|
|
22
|
+
const roleServicePath = resolveParentDistModule('config', 'role-service.js');
|
|
23
23
|
const authPath = resolveParentDistModule('core', 'auth', 'auth-gateway.js');
|
|
24
24
|
// Windows 上需要转换为 file:// URL
|
|
25
25
|
try {
|
|
26
26
|
const rolesMod = await import(toFileUrl(rolesPath));
|
|
27
27
|
const cmMod = await import(toFileUrl(cmPath));
|
|
28
28
|
const resolverMod = await import(toFileUrl(resolverPath));
|
|
29
|
+
const roleServiceMod = await import(toFileUrl(roleServicePath));
|
|
29
30
|
const authMod = await import(toFileUrl(authPath));
|
|
30
31
|
if (!rolesMod.readRolesConfig)
|
|
31
32
|
throw new Error('readRolesConfig not found in roles.js');
|
|
32
|
-
if (!rolesMod.getBuiltinRolesConfig)
|
|
33
|
-
throw new Error('getBuiltinRolesConfig not found in roles.js');
|
|
34
33
|
if (!rolesMod.roleExists)
|
|
35
34
|
throw new Error('roleExists not found in roles.js');
|
|
36
35
|
if (!cmMod.read)
|
|
@@ -43,9 +42,13 @@ async function getRolesModule() {
|
|
|
43
42
|
throw new Error('buildAuthSubject not found in auth-gateway.js');
|
|
44
43
|
if (!authMod.authorizeOperation)
|
|
45
44
|
throw new Error('authorizeOperation not found in auth-gateway.js');
|
|
45
|
+
if (!roleServiceMod.readRoleRegistry || !roleServiceMod.writeRoleRegistry
|
|
46
|
+
|| !roleServiceMod.readRoleDefinition || !roleServiceMod.writeRoleDefinition
|
|
47
|
+
|| !roleServiceMod.registerRole || !roleServiceMod.unregisterRole
|
|
48
|
+
|| !roleServiceMod.resetRoleDefinition)
|
|
49
|
+
throw new Error('role-service exports are incomplete');
|
|
46
50
|
return {
|
|
47
51
|
readRolesConfig: rolesMod.readRolesConfig,
|
|
48
|
-
getBuiltinRolesConfig: rolesMod.getBuiltinRolesConfig,
|
|
49
52
|
roleExists: rolesMod.roleExists,
|
|
50
53
|
read: cmMod.read,
|
|
51
54
|
write: cmMod.write,
|
|
@@ -53,6 +56,7 @@ async function getRolesModule() {
|
|
|
53
56
|
resolver: resolverMod,
|
|
54
57
|
buildAuthSubject: authMod.buildAuthSubject,
|
|
55
58
|
authorizeOperation: authMod.authorizeOperation,
|
|
59
|
+
roleService: roleServiceMod,
|
|
56
60
|
};
|
|
57
61
|
}
|
|
58
62
|
catch (err) {
|
|
@@ -78,78 +82,11 @@ async function getOperationRegistryModule() {
|
|
|
78
82
|
listOperations: mod.listOperations,
|
|
79
83
|
};
|
|
80
84
|
}
|
|
81
|
-
async function execRoleMenu(aid, payload, auth) {
|
|
82
|
-
const response = await ipcQuery(resolvePaths().socket, {
|
|
83
|
-
type: 'menu.exec',
|
|
84
|
-
payload: { ...payload, name: 'role', agent: aid, args: { self: aid, ...(payload.args ?? {}) } },
|
|
85
|
-
auth: { source: 'ecweb', actorAid: auth.actorAid ?? null, localDirect: !!auth.localDirect },
|
|
86
|
-
}, 10_000);
|
|
87
|
-
if (!response?.ok || !response.response)
|
|
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;
|
|
118
|
-
}
|
|
119
85
|
export function visibleRoleDefinitionOptions(roleOptions) {
|
|
120
86
|
return roleOptions
|
|
121
87
|
.filter(item => item?.kind === 'user' || item?.value === 'admin')
|
|
122
88
|
.sort((left, right) => Number(right.value === 'admin') - Number(left.value === 'admin'));
|
|
123
89
|
}
|
|
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
90
|
function isWriteMethod(method) {
|
|
154
91
|
return method === 'PUT' || method === 'POST' || method === 'DELETE';
|
|
155
92
|
}
|
|
@@ -160,9 +97,10 @@ async function authorizeRolePolicyOperation(modules, processConfig, auth, aid, o
|
|
|
160
97
|
const actor = auth.actorAid || '';
|
|
161
98
|
const processOwners = Array.isArray(processConfig?.owners) ? processConfig.owners.map(String) : [];
|
|
162
99
|
const syntheticOwner = !!auth.localDirect || (!!actor && processOwners.includes(actor));
|
|
100
|
+
const actorId = auth.localDirect ? 'local-direct' : actor || undefined;
|
|
163
101
|
const subject = modules.buildAuthSubject({
|
|
164
102
|
selfAid: aid,
|
|
165
|
-
actorId
|
|
103
|
+
actorId,
|
|
166
104
|
channel: `ecweb#${aid}`,
|
|
167
105
|
channelType: 'aun',
|
|
168
106
|
channelId: actor || 'ecweb',
|
|
@@ -176,6 +114,19 @@ async function authorizeRolePolicyOperation(modules, processConfig, auth, aid, o
|
|
|
176
114
|
isAuthenticated: true,
|
|
177
115
|
allowAccess: true,
|
|
178
116
|
roleExists: true,
|
|
117
|
+
// buildAuthSubject uses the authenticated actor separately from the
|
|
118
|
+
// resolved relation role to determine daemon-owner provenance.
|
|
119
|
+
actor: {
|
|
120
|
+
operatorId: actorId || 'ecweb',
|
|
121
|
+
operatorChannelKey: `ecweb#${aid}`,
|
|
122
|
+
operatorChannelType: 'aun',
|
|
123
|
+
...(actor ? {
|
|
124
|
+
principalId: actor,
|
|
125
|
+
principalStatus: 'native_aid',
|
|
126
|
+
} : {
|
|
127
|
+
principalStatus: 'unmapped',
|
|
128
|
+
}),
|
|
129
|
+
},
|
|
179
130
|
},
|
|
180
131
|
} : {}),
|
|
181
132
|
});
|
|
@@ -193,6 +144,15 @@ async function authorizeRolePolicyOperation(modules, processConfig, auth, aid, o
|
|
|
193
144
|
},
|
|
194
145
|
});
|
|
195
146
|
}
|
|
147
|
+
function isDaemonOwnerAuth(processConfig, auth) {
|
|
148
|
+
if (auth.localDirect)
|
|
149
|
+
return true;
|
|
150
|
+
const actor = typeof auth.actorAid === 'string' ? auth.actorAid.trim() : '';
|
|
151
|
+
const processOwners = Array.isArray(processConfig?.owners)
|
|
152
|
+
? processConfig.owners.map((value) => String(value || '').trim()).filter(Boolean)
|
|
153
|
+
: [];
|
|
154
|
+
return !!actor && processOwners.includes(actor);
|
|
155
|
+
}
|
|
196
156
|
const MODEL_PERMISSION_FIELD = 'baseagents.claude.model';
|
|
197
157
|
const MODEL_PATTERN_OPTIONS = ['*', 'claude-opus-*', 'claude-sonnet-*', 'claude-haiku-*'];
|
|
198
158
|
const COMMAND_PERMISSION_SCOPES = new Set(['relation', 'role', 'agent', 'process', 'filesystem', 'control', 'raw-cli']);
|
|
@@ -313,33 +273,24 @@ function isReservedRoleName(roleName) {
|
|
|
313
273
|
function isValidUserRoleName(roleName) {
|
|
314
274
|
return /^[a-z0-9_-]+$/.test(roleName) && !isReservedRoleName(roleName);
|
|
315
275
|
}
|
|
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
|
-
: {};
|
|
276
|
+
function writeRoleRegistry(modules, aid, defaultRoles) {
|
|
277
|
+
const registry = modules.roleService.readRoleRegistry(aid);
|
|
278
|
+
modules.roleService.writeRoleRegistry(aid, {
|
|
279
|
+
...registry,
|
|
280
|
+
defaultRoles: {
|
|
281
|
+
private: defaultRoles.private ?? null,
|
|
282
|
+
group: defaultRoles.group ?? null,
|
|
283
|
+
},
|
|
284
|
+
});
|
|
329
285
|
}
|
|
330
|
-
function
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
roles.definitions = { ...roles.definitions };
|
|
286
|
+
function writeRoleDefinition(modules, aid, roleName, definition) {
|
|
287
|
+
const registry = modules.roleService.readRoleRegistry(aid);
|
|
288
|
+
if (!registry.roles.includes(roleName)) {
|
|
289
|
+
modules.roleService.registerRole(aid, roleName, definition);
|
|
335
290
|
}
|
|
336
|
-
|
|
337
|
-
|
|
291
|
+
else {
|
|
292
|
+
modules.roleService.writeRoleDefinition(aid, roleName, definition);
|
|
338
293
|
}
|
|
339
|
-
updater(roles, config);
|
|
340
|
-
const next = { ...config, roles };
|
|
341
|
-
writeAgentConfig(modules, aid, next);
|
|
342
|
-
return next;
|
|
343
294
|
}
|
|
344
295
|
function normalizeDefaultRoleInput(value, current) {
|
|
345
296
|
if (value === null || value === '')
|
|
@@ -627,7 +578,33 @@ async function buildSnapshot(params = {}, auth = {}) {
|
|
|
627
578
|
const aid = resolveRequestedAid(params.aid, agents);
|
|
628
579
|
if (!aid)
|
|
629
580
|
return { $schema_version: 1, aid: null, agents: [], roles: {}, defaultRoles: { private: null, group: null } };
|
|
630
|
-
|
|
581
|
+
const modules = await getRolesModule();
|
|
582
|
+
const processConfig = modules.read(modules.ConfigTarget.Process) || {};
|
|
583
|
+
const decision = await authorizeRolePolicyOperation(modules, processConfig, auth, aid, 'role.policy.read', {
|
|
584
|
+
method: 'GET',
|
|
585
|
+
path: '/api/role-definitions',
|
|
586
|
+
});
|
|
587
|
+
if (!decision.allow) {
|
|
588
|
+
return {
|
|
589
|
+
$schema_version: 1,
|
|
590
|
+
aid,
|
|
591
|
+
agents: summarizeAgents(agents),
|
|
592
|
+
roles: {},
|
|
593
|
+
defaultRoles: { private: null, group: null },
|
|
594
|
+
error: decision.reason || 'forbidden: role policy read not allowed',
|
|
595
|
+
code: decision.code,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
const config = modules.readRolesConfig(aid);
|
|
599
|
+
return {
|
|
600
|
+
$schema_version: 1,
|
|
601
|
+
aid,
|
|
602
|
+
self: aid,
|
|
603
|
+
agents: summarizeAgents(agents),
|
|
604
|
+
defaultRoles: config.defaultRoles ?? { private: null, group: null },
|
|
605
|
+
policyRevision: JSON.stringify(config),
|
|
606
|
+
roles: config.roles ?? {},
|
|
607
|
+
};
|
|
631
608
|
}
|
|
632
609
|
catch (err) {
|
|
633
610
|
console.error('[role-definitions] Failed to build snapshot:', err);
|
|
@@ -643,183 +620,6 @@ export const roleDefinitionsSource = {
|
|
|
643
620
|
return () => { };
|
|
644
621
|
}
|
|
645
622
|
};
|
|
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
623
|
// HTTP API handler
|
|
824
624
|
export async function handleRoleDefinitionsApi(req, res, auth = {}) {
|
|
825
625
|
try {
|
|
@@ -827,14 +627,8 @@ export async function handleRoleDefinitionsApi(req, res, auth = {}) {
|
|
|
827
627
|
const urlPath = parsedUrl.path;
|
|
828
628
|
const agents = await getAgentsFromIpc();
|
|
829
629
|
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
630
|
const modules = await getRolesModule();
|
|
837
|
-
const { readRolesConfig,
|
|
631
|
+
const { readRolesConfig, read, ConfigTarget } = modules;
|
|
838
632
|
const roleRoute = urlPath.match(/^\/api\/role-definitions\/([^/]+)$/);
|
|
839
633
|
const roleNestedRoute = urlPath.match(/^\/api\/role-definitions\/([^/]+)\/([^/]+)$/);
|
|
840
634
|
const decodeRoleName = (raw) => {
|
|
@@ -881,9 +675,25 @@ export async function handleRoleDefinitionsApi(req, res, auth = {}) {
|
|
|
881
675
|
});
|
|
882
676
|
return;
|
|
883
677
|
}
|
|
678
|
+
if (isWriteMethod(req.method) && !isDaemonOwnerAuth(processConfig, auth)
|
|
679
|
+
&& decision.subject?.role === 'admin') {
|
|
680
|
+
const targetedRole = roleNestedRoute
|
|
681
|
+
? decodeRoleName(roleNestedRoute[1])
|
|
682
|
+
: roleRoute
|
|
683
|
+
? decodeRoleName(roleRoute[1])
|
|
684
|
+
: null;
|
|
685
|
+
if (targetedRole === 'owner') {
|
|
686
|
+
sendJson(res, 403, {
|
|
687
|
+
error: 'forbidden: agent admin cannot modify owner role policy',
|
|
688
|
+
code: 'PERMISSION_DENIED',
|
|
689
|
+
actorRole: 'admin',
|
|
690
|
+
});
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
884
694
|
}
|
|
885
695
|
if (req.method === 'GET' && urlPath === '/api/role-definitions') {
|
|
886
|
-
sendJson(res, 200, await buildSnapshot({ aid }));
|
|
696
|
+
sendJson(res, 200, await buildSnapshot({ aid }, auth));
|
|
887
697
|
return;
|
|
888
698
|
}
|
|
889
699
|
if (req.method === 'PUT' && urlPath === '/api/role-definitions') {
|
|
@@ -914,22 +724,38 @@ export async function handleRoleDefinitionsApi(req, res, auth = {}) {
|
|
|
914
724
|
}
|
|
915
725
|
}
|
|
916
726
|
}
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
private: normalizeDefaultRoleInput(incoming.defaultRoles.private, currentConfig.defaultRoles?.private),
|
|
921
|
-
group: normalizeDefaultRoleInput(incoming.defaultRoles.group, currentConfig.defaultRoles?.group),
|
|
922
|
-
};
|
|
727
|
+
if (incoming.roles && typeof incoming.roles === 'object') {
|
|
728
|
+
for (const [roleName, roleDef] of Object.entries(incoming.roles)) {
|
|
729
|
+
writeRoleDefinition(modules, aid, roleName, normalizeRoleDefinitionForSave(roleDef));
|
|
923
730
|
}
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
731
|
+
}
|
|
732
|
+
if (incoming.defaultRoles && typeof incoming.defaultRoles === 'object') {
|
|
733
|
+
const nextDefaults = {
|
|
734
|
+
private: normalizeDefaultRoleInput(incoming.defaultRoles.private, currentConfig.defaultRoles?.private),
|
|
735
|
+
group: normalizeDefaultRoleInput(incoming.defaultRoles.group, currentConfig.defaultRoles?.group),
|
|
736
|
+
};
|
|
737
|
+
const processConfig = modules.read(modules.ConfigTarget.Process) || {};
|
|
738
|
+
const daemonOwner = isDaemonOwnerAuth(processConfig, auth);
|
|
739
|
+
const actorRole = daemonOwner
|
|
740
|
+
? 'owner'
|
|
741
|
+
: auth.actorAid
|
|
742
|
+
? (modules.resolver.resolvePeerRoleDetail({
|
|
743
|
+
selfAid: aid,
|
|
744
|
+
channelType: 'aun',
|
|
745
|
+
chatType: 'private',
|
|
746
|
+
actorId: auth.actorAid,
|
|
747
|
+
conversationId: auth.actorAid,
|
|
748
|
+
}).effectiveRole || 'none')
|
|
749
|
+
: 'none';
|
|
750
|
+
const changesAdminDefault = ['private', 'group'].some(scene => currentConfig.defaultRoles?.[scene] !== nextDefaults[scene]
|
|
751
|
+
&& (currentConfig.defaultRoles?.[scene] === 'admin' || nextDefaults[scene] === 'admin'));
|
|
752
|
+
if (!daemonOwner && actorRole !== 'owner' && changesAdminDefault) {
|
|
753
|
+
sendJson(res, 403, { error: 'Only an owner can set or replace an admin default role' });
|
|
754
|
+
return;
|
|
930
755
|
}
|
|
931
|
-
|
|
932
|
-
|
|
756
|
+
writeRoleRegistry(modules, aid, nextDefaults);
|
|
757
|
+
}
|
|
758
|
+
sendJson(res, 200, { ok: true, data: await buildSnapshot({ aid }, auth) });
|
|
933
759
|
}
|
|
934
760
|
catch (err) {
|
|
935
761
|
console.error('[role-definitions] Failed to update agent role config:', err);
|
|
@@ -975,9 +801,7 @@ export async function handleRoleDefinitionsApi(req, res, auth = {}) {
|
|
|
975
801
|
if (usageLimits !== undefined) {
|
|
976
802
|
newRole.usageLimits = normalizeRoleUsageLimits(usageLimits);
|
|
977
803
|
}
|
|
978
|
-
|
|
979
|
-
roles.definitions = { ...(roles.definitions || {}), [name]: newRole };
|
|
980
|
-
});
|
|
804
|
+
modules.roleService.registerRole(aid, name, newRole);
|
|
981
805
|
sendJson(res, 201, { ok: true, role: newRole });
|
|
982
806
|
}
|
|
983
807
|
catch (err) {
|
|
@@ -1042,30 +866,19 @@ export async function handleRoleDefinitionsApi(req, res, auth = {}) {
|
|
|
1042
866
|
allowOverride: validation.value.allowOverride,
|
|
1043
867
|
allowedModels: validation.value.allowedModels,
|
|
1044
868
|
};
|
|
1045
|
-
|
|
1046
|
-
roles.definitions = { ...(roles.definitions || {}), [roleName]: roleDef };
|
|
1047
|
-
});
|
|
869
|
+
modules.roleService.writeRoleDefinition(aid, roleName, roleDef);
|
|
1048
870
|
const data = await buildModelPermissionResponse(roleName, roleDef, validation.value);
|
|
1049
871
|
sendJson(res, 200, { success: true, data });
|
|
1050
872
|
return;
|
|
1051
873
|
}
|
|
1052
874
|
if (req.method === 'POST' && roleNestedRoute?.[2] === 'reset') {
|
|
1053
875
|
const roleName = decodeRoleName(roleNestedRoute[1]);
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
if (!builtinRole) {
|
|
1057
|
-
sendJson(res, 404, { error: 'Role not found in builtin config' });
|
|
876
|
+
if (!['admin', 'member', 'visitor'].includes(roleName)) {
|
|
877
|
+
sendJson(res, 403, { error: 'Only builtin role overrides can be reset' });
|
|
1058
878
|
return;
|
|
1059
879
|
}
|
|
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 });
|
|
880
|
+
modules.roleService.resetRoleDefinition(aid, roleName);
|
|
881
|
+
sendJson(res, 200, { ok: true, role: readRolesConfig(aid).roles[roleName] });
|
|
1069
882
|
return;
|
|
1070
883
|
}
|
|
1071
884
|
if (req.method === 'GET' && roleRoute) {
|
|
@@ -1081,25 +894,21 @@ export async function handleRoleDefinitionsApi(req, res, auth = {}) {
|
|
|
1081
894
|
}
|
|
1082
895
|
if (req.method === 'DELETE' && roleRoute) {
|
|
1083
896
|
const roleName = decodeRoleName(roleRoute[1]);
|
|
1084
|
-
|
|
1085
|
-
if (builtinRoles.has(roleName) || isReservedRoleName(roleName)) {
|
|
897
|
+
if (['owner', 'admin', 'member', 'visitor'].includes(roleName)) {
|
|
1086
898
|
sendJson(res, 403, { error: 'Cannot delete builtin or management role' });
|
|
1087
899
|
return;
|
|
1088
900
|
}
|
|
1089
|
-
const
|
|
1090
|
-
|
|
1091
|
-
if (!definitions[roleName]) {
|
|
901
|
+
const config = readRolesConfig(aid);
|
|
902
|
+
if (!config.roles[roleName]) {
|
|
1092
903
|
sendJson(res, 404, { error: 'Role not found' });
|
|
1093
904
|
return;
|
|
1094
905
|
}
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
delete roles.definitions;
|
|
1102
|
-
});
|
|
906
|
+
const replacement = Object.prototype.hasOwnProperty.call(parsedUrl.query, 'replacementRoleId')
|
|
907
|
+
? (parsedUrl.query.replacementRoleId || null)
|
|
908
|
+
: parsedUrl.query.clearReferences === 'true'
|
|
909
|
+
? null
|
|
910
|
+
: undefined;
|
|
911
|
+
modules.roleService.unregisterRole(aid, roleName, replacement);
|
|
1103
912
|
sendJson(res, 200, { ok: true });
|
|
1104
913
|
return;
|
|
1105
914
|
}
|
|
@@ -1157,9 +966,7 @@ export async function handleRoleDefinitionsApi(req, res, auth = {}) {
|
|
|
1157
966
|
sendJson(res, 400, { error: 'Invalid model permissions', errors: validation.errors });
|
|
1158
967
|
return;
|
|
1159
968
|
}
|
|
1160
|
-
|
|
1161
|
-
roles.definitions = { ...(roles.definitions || {}), [roleName]: nextRole };
|
|
1162
|
-
});
|
|
969
|
+
modules.roleService.writeRoleDefinition(aid, roleName, nextRole);
|
|
1163
970
|
sendJson(res, 200, { ok: true });
|
|
1164
971
|
}
|
|
1165
972
|
catch (err) {
|