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.
@@ -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: auth.localDirect ? 'local-direct' : actor || undefined,
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 builtinUserRoles(modules) {
317
- return new Set(Object.keys(modules.getBuiltinRolesConfig().roles || {}));
318
- }
319
- function readAgentConfig(modules, aid) {
320
- return modules.read(modules.ConfigTarget.Agent, { self: aid }) || { aid, channels: [] };
321
- }
322
- function writeAgentConfig(modules, aid, config) {
323
- modules.write(modules.ConfigTarget.Agent, { ...config, aid, channels: Array.isArray(config.channels) ? config.channels : [] }, { self: aid });
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 updateAgentRoles(modules, aid, updater) {
331
- const config = readAgentConfig(modules, aid);
332
- const roles = config.roles && typeof config.roles === 'object' ? { ...config.roles } : {};
333
- if (roles.definitions && typeof roles.definitions === 'object') {
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
- if (roles.defaultRoles && typeof roles.defaultRoles === 'object') {
337
- roles.defaultRoles = { ...roles.defaultRoles };
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
- return await buildMenuSnapshot(aid, agents, auth);
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, getBuiltinRolesConfig, read, ConfigTarget } = modules;
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
- updateAgentRoles(modules, aid, (roles) => {
918
- if (incoming.defaultRoles && typeof incoming.defaultRoles === 'object') {
919
- roles.defaultRoles = {
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
- if (incoming.roles && typeof incoming.roles === 'object') {
925
- const nextDefinitions = {};
926
- for (const [roleName, roleDef] of Object.entries(incoming.roles)) {
927
- nextDefinitions[roleName] = normalizeRoleDefinitionForSave(roleDef);
928
- }
929
- roles.definitions = nextDefinitions;
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
- sendJson(res, 200, { ok: true, data: await buildSnapshot({ aid }) });
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
- updateAgentRoles(modules, aid, (roles) => {
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
- updateAgentRoles(modules, aid, (roles) => {
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
- const builtinConfig = getBuiltinRolesConfig();
1055
- const builtinRole = builtinConfig.roles[roleName];
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
- updateAgentRoles(modules, aid, (roles) => {
1061
- const definitions = { ...(roles.definitions || {}) };
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
- const builtinRoles = builtinUserRoles(modules);
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 agentConfig = readAgentConfig(modules, aid);
1090
- const definitions = getAgentRoleDefinitions(agentConfig);
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
- updateAgentRoles(modules, aid, (roles) => {
1096
- const nextDefinitions = { ...(roles.definitions || {}) };
1097
- delete nextDefinitions[roleName];
1098
- if (Object.keys(nextDefinitions).length)
1099
- roles.definitions = nextDefinitions;
1100
- else
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
- updateAgentRoles(modules, aid, (roles) => {
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) {