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.
@@ -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 { ipcQuery } from '../ipc-client.js';
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
- 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;
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: auth.localDirect ? 'local-direct' : actor || undefined,
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 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
- : {};
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 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 };
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
- if (roles.defaultRoles && typeof roles.defaultRoles === 'object') {
337
- roles.defaultRoles = { ...roles.defaultRoles };
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
- return await buildMenuSnapshot(aid, agents, auth);
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, params.__menuAuth ?? {});
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, getBuiltinRolesConfig, read, ConfigTarget } = modules;
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
- 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
- };
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
- 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;
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
- sendJson(res, 200, { ok: true, data: await buildSnapshot({ aid }) });
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
- updateAgentRoles(modules, aid, (roles) => {
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
- updateAgentRoles(modules, aid, (roles) => {
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
- const builtinConfig = getBuiltinRolesConfig();
1055
- const builtinRole = builtinConfig.roles[roleName];
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
- 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 });
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
- const builtinRoles = builtinUserRoles(modules);
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 agentConfig = readAgentConfig(modules, aid);
1090
- const definitions = getAgentRoleDefinitions(agentConfig);
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
- 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
- });
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
- updateAgentRoles(modules, aid, (roles) => {
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) {