ec-web 0.0.21 → 0.0.23

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,6 +14,7 @@
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 crypto from 'node:crypto';
17
18
  // 动态导入 evolcore 主项目的角色配置模块
18
19
  async function getRolesModule() {
19
20
  const rolesPath = resolveParentDistModule('config', 'roles.js');
@@ -82,6 +83,14 @@ async function getOperationRegistryModule() {
82
83
  listOperations: mod.listOperations,
83
84
  };
84
85
  }
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
+ };
93
+ }
85
94
  export function visibleRoleDefinitionOptions(roleOptions) {
86
95
  return roleOptions
87
96
  .filter(item => item?.kind === 'user' || item?.value === 'admin')
@@ -142,6 +151,7 @@ async function authorizeRolePolicyOperation(modules, processConfig, auth, aid, o
142
151
  ...args,
143
152
  },
144
153
  },
154
+ auditMetadata: ecwebAuditContext(auth, aid, operation),
145
155
  });
146
156
  }
147
157
  function isDaemonOwnerAuth(processConfig, auth) {
@@ -268,7 +278,7 @@ function resolveRequestedAid(raw, agents) {
268
278
  return agents.find(agent => agent?.aid)?.aid || null;
269
279
  }
270
280
  function isReservedRoleName(roleName) {
271
- return roleName === 'owner' || roleName === 'admin';
281
+ return roleName === 'owner' || roleName === 'admin' || roleName === 'daemon-owner';
272
282
  }
273
283
  function isValidUserRoleName(roleName) {
274
284
  return /^[a-z0-9_-]+$/.test(roleName) && !isReservedRoleName(roleName);
@@ -299,11 +309,12 @@ function normalizeDefaultRoleInput(value, current) {
299
309
  return value;
300
310
  return current ?? null;
301
311
  }
302
- function validateRoleDefinitionName(roleName) {
312
+ function validateRoleDefinitionName(roleName, options = {}) {
303
313
  if (!roleName || !/^[a-z0-9_-]+$/.test(roleName))
304
314
  return 'Invalid role name';
305
- if (isReservedRoleName(roleName))
306
- return 'owner/admin are management identities and cannot be user role definitions';
315
+ if (isReservedRoleName(roleName) && (!options.allowManagement || roleName === 'daemon-owner')) {
316
+ return 'owner/admin/daemon-owner are management identities and cannot be user role definitions';
317
+ }
307
318
  return null;
308
319
  }
309
320
  function inferSelectionMode(allowedModels = []) {
@@ -547,12 +558,12 @@ async function filterAllowedCatalogModels(models, allowedModels) {
547
558
  const { isModelAllowedByPatterns } = await getRoleConstraintsModule();
548
559
  return models.filter(model => !model.isAlias && isModelAllowedByPatterns(model.id, allowedModels));
549
560
  }
550
- async function buildModelPermissionResponse(roleName, roleDef, override) {
561
+ async function buildModelPermissionResponse(roleName, roleDef, override, aid) {
551
562
  const currentPerm = getModelPermission(roleDef);
552
563
  const defaultModel = override?.defaultModel ?? currentPerm.default ?? '';
553
564
  const allowOverride = override?.allowOverride ?? currentPerm.allowOverride ?? false;
554
565
  const allowedModels = override?.allowedModels ?? (Array.isArray(currentPerm.allowedModels) ? currentPerm.allowedModels : []);
555
- const catalog = await getModelCatalogSnapshot('claude');
566
+ const catalog = await getModelCatalogSnapshot('claude', aid);
556
567
  const matchingModels = await filterAllowedCatalogModels(catalog.models, allowedModels);
557
568
  return {
558
569
  role: roleName,
@@ -574,6 +585,11 @@ async function buildModelPermissionResponse(roleName, roleDef, override) {
574
585
  }
575
586
  async function buildSnapshot(params = {}, auth = {}) {
576
587
  try {
588
+ auth = {
589
+ ...auth,
590
+ ecwebRequestId: params.__ecwebRequestId ?? auth.ecwebRequestId ?? crypto.randomUUID(),
591
+ endpoint: params.__ecwebEndpoint ?? auth.endpoint ?? 'WS snapshot:roleDefinitions',
592
+ };
577
593
  const agents = await getAgentsFromIpc();
578
594
  const aid = resolveRequestedAid(params.aid, agents);
579
595
  if (!aid)
@@ -614,7 +630,11 @@ async function buildSnapshot(params = {}, auth = {}) {
614
630
  export const roleDefinitionsSource = {
615
631
  kind: 'roleDefinitions',
616
632
  async snapshot(params = {}) {
617
- return buildSnapshot(params, params.__menuAuth ?? {});
633
+ return buildSnapshot(params, {
634
+ ...(params.__menuAuth ?? {}),
635
+ ecwebRequestId: params.__ecwebRequestId,
636
+ endpoint: params.__ecwebEndpoint,
637
+ });
618
638
  },
619
639
  subscribe(_params, _push) {
620
640
  return () => { };
@@ -625,10 +645,16 @@ export async function handleRoleDefinitionsApi(req, res, auth = {}) {
625
645
  try {
626
646
  const parsedUrl = parseUrl(req.url || '');
627
647
  const urlPath = parsedUrl.path;
648
+ auth = {
649
+ ...auth,
650
+ ecwebRequestId: auth.ecwebRequestId ?? crypto.randomUUID(),
651
+ endpoint: auth.endpoint ?? `${req.method ?? 'UNKNOWN'} ${urlPath}`,
652
+ };
628
653
  const agents = await getAgentsFromIpc();
629
654
  const aid = resolveRequestedAid(parsedUrl.query.aid, agents);
630
655
  const modules = await getRolesModule();
631
656
  const { readRolesConfig, read, ConfigTarget } = modules;
657
+ let authorizationDecision = null;
632
658
  const roleRoute = urlPath.match(/^\/api\/role-definitions\/([^/]+)$/);
633
659
  const roleNestedRoute = urlPath.match(/^\/api\/role-definitions\/([^/]+)\/([^/]+)$/);
634
660
  const decodeRoleName = (raw) => {
@@ -655,6 +681,7 @@ export async function handleRoleDefinitionsApi(req, res, auth = {}) {
655
681
  method: req.method,
656
682
  path: urlPath,
657
683
  });
684
+ authorizationDecision = decision;
658
685
  if (!decision.allow) {
659
686
  console.warn('[role-definitions] Role policy access forbidden:', {
660
687
  method: req.method,
@@ -693,20 +720,40 @@ export async function handleRoleDefinitionsApi(req, res, auth = {}) {
693
720
  }
694
721
  }
695
722
  if (req.method === 'GET' && urlPath === '/api/role-definitions') {
696
- sendJson(res, 200, await buildSnapshot({ aid }, auth));
723
+ sendJson(res, 200, await buildSnapshot({ aid, __ecwebRequestId: auth.ecwebRequestId, __ecwebEndpoint: auth.endpoint }, auth));
697
724
  return;
698
725
  }
699
726
  if (req.method === 'PUT' && urlPath === '/api/role-definitions') {
700
727
  try {
701
728
  const incoming = await readJsonBody(req);
702
729
  const currentConfig = readRolesConfig(aid || undefined);
730
+ // A complete snapshot may contain both management identities. Keep the
731
+ // same hierarchy guard used by the per-role endpoint for bulk updates.
732
+ if (!isDaemonOwnerAuth(read(ConfigTarget.Process) || {}, auth)
733
+ && authorizationDecision?.subject?.role === 'admin'
734
+ && incoming.roles && typeof incoming.roles === 'object'
735
+ && Object.prototype.hasOwnProperty.call(incoming.roles, 'owner')) {
736
+ sendJson(res, 403, {
737
+ error: 'forbidden: agent admin cannot modify owner role policy',
738
+ code: 'PERMISSION_DENIED',
739
+ actorRole: 'admin',
740
+ });
741
+ return;
742
+ }
703
743
  if (incoming.roles && typeof incoming.roles === 'object') {
704
744
  for (const [roleName, roleDef] of Object.entries(incoming.roles)) {
705
- const nameError = validateRoleDefinitionName(roleName);
745
+ // The collection endpoint receives the complete snapshot, including
746
+ // management-role overrides. Creation of new management identities
747
+ // remains forbidden; updates are narrowed by the runtime ceiling.
748
+ const nameError = validateRoleDefinitionName(roleName, { allowManagement: true });
706
749
  if (nameError) {
707
750
  sendJson(res, 400, { error: `${nameError}: ${roleName}` });
708
751
  return;
709
752
  }
753
+ if (isReservedRoleName(roleName) && !Object.prototype.hasOwnProperty.call(currentConfig.roles, roleName)) {
754
+ sendJson(res, 404, { error: `Management role not found: ${roleName}` });
755
+ return;
756
+ }
710
757
  const validation = await validateRoleModelPermission(roleDef);
711
758
  if (!validation.ok) {
712
759
  sendJson(res, 400, { error: `Invalid model permissions for role ${roleName}`, errors: validation.errors });
@@ -755,7 +802,7 @@ export async function handleRoleDefinitionsApi(req, res, auth = {}) {
755
802
  }
756
803
  writeRoleRegistry(modules, aid, nextDefaults);
757
804
  }
758
- sendJson(res, 200, { ok: true, data: await buildSnapshot({ aid }, auth) });
805
+ sendJson(res, 200, { ok: true, data: await buildSnapshot({ aid, __ecwebRequestId: auth.ecwebRequestId, __ecwebEndpoint: auth.endpoint }, auth) });
759
806
  }
760
807
  catch (err) {
761
808
  console.error('[role-definitions] Failed to update agent role config:', err);
@@ -818,7 +865,7 @@ export async function handleRoleDefinitionsApi(req, res, auth = {}) {
818
865
  sendJson(res, 404, { success: false, error: 'Role not found' });
819
866
  return;
820
867
  }
821
- const data = await buildModelPermissionResponse(roleName, roleDef);
868
+ const data = await buildModelPermissionResponse(roleName, roleDef, undefined, aid || undefined);
822
869
  sendJson(res, 200, { success: true, data });
823
870
  return;
824
871
  }
@@ -837,7 +884,7 @@ export async function handleRoleDefinitionsApi(req, res, auth = {}) {
837
884
  sendJson(res, 400, { success: false, error: 'Invalid model permissions', errors: validation.errors });
838
885
  return;
839
886
  }
840
- const data = await buildModelPermissionResponse(roleName, roleDef, validation.value);
887
+ const data = await buildModelPermissionResponse(roleName, roleDef, validation.value, aid || undefined);
841
888
  sendJson(res, 200, { success: true, data });
842
889
  return;
843
890
  }
@@ -867,13 +914,13 @@ export async function handleRoleDefinitionsApi(req, res, auth = {}) {
867
914
  allowedModels: validation.value.allowedModels,
868
915
  };
869
916
  modules.roleService.writeRoleDefinition(aid, roleName, roleDef);
870
- const data = await buildModelPermissionResponse(roleName, roleDef, validation.value);
917
+ const data = await buildModelPermissionResponse(roleName, roleDef, validation.value, aid || undefined);
871
918
  sendJson(res, 200, { success: true, data });
872
919
  return;
873
920
  }
874
921
  if (req.method === 'POST' && roleNestedRoute?.[2] === 'reset') {
875
922
  const roleName = decodeRoleName(roleNestedRoute[1]);
876
- if (!['admin', 'member', 'visitor'].includes(roleName)) {
923
+ if (!['owner', 'admin', 'member', 'visitor'].includes(roleName)) {
877
924
  sendJson(res, 403, { error: 'Only builtin role overrides can be reset' });
878
925
  return;
879
926
  }
@@ -916,7 +963,9 @@ export async function handleRoleDefinitionsApi(req, res, auth = {}) {
916
963
  try {
917
964
  const roleName = decodeRoleName(roleRoute[1]);
918
965
  const updates = await readJsonBody(req);
919
- const nameError = validateRoleDefinitionName(roleName);
966
+ // owner/admin are reserved identities, but their policy definitions may
967
+ // be overridden (subject to the management-role ceiling).
968
+ const nameError = validateRoleDefinitionName(roleName, { allowManagement: true });
920
969
  if (nameError) {
921
970
  sendJson(res, 400, { error: nameError });
922
971
  return;
@@ -581,20 +581,32 @@ function approvalAuditFromPayload(payload) {
581
581
  }
582
582
  function classifyCodexToolRejection(text, payload) {
583
583
  const normalized = text.replace(/\\(["'])/g, '$1');
584
- if (!/Rejected\(\s*["']rejected by user["']\s*\)/i.test(normalized))
584
+ const hookMatch = normalized.match(/(?:Command|Tool call) blocked by PreToolUse hook:\s*(.+?)(?:\.\s+(?:Command|Tool):|$)/is);
585
+ const genericRejected = /Rejected\(\s*["']rejected by user["']\s*\)/i.test(normalized);
586
+ if (!hookMatch && !genericRejected)
585
587
  return undefined;
586
588
  const audit = approvalAuditFromPayload(payload);
587
- const source = audit.policyCode || audit.decisionSource?.toLowerCase().includes('policy')
589
+ const source = hookMatch || audit.policyCode || audit.decisionSource?.toLowerCase().includes('policy')
588
590
  ? 'evolcore-policy'
589
591
  : 'codex-approval';
592
+ const backendReason = hookMatch?.[1]?.trim()
593
+ || 'Rejected("rejected by user")';
594
+ const inferredPolicyCode = audit.policyCode
595
+ || (hookMatch && /工具调用已达到\s*\d+\s*次/.test(backendReason)
596
+ ? 'proactive_tool_report_required'
597
+ : hookMatch && /请先用\s+ec\s+(?:group\s+send|msg\s+send)/.test(backendReason)
598
+ ? 'proactive_first_send_required'
599
+ : undefined);
590
600
  return {
591
- kind: 'approval_rejected',
601
+ kind: source === 'evolcore-policy' ? 'policy_blocked' : 'approval_rejected',
592
602
  source,
593
- reason: 'rejected_by_user',
603
+ reason: source === 'evolcore-policy' ? 'policy_denied' : 'rejected_by_user',
594
604
  executed: false,
595
605
  evidence: audit.available ? 'structured_metadata' : 'backend_text',
596
- backendReason: 'Rejected("rejected by user")',
597
- audit,
606
+ backendReason,
607
+ audit: inferredPolicyCode && !audit.policyCode
608
+ ? { ...audit, policyCode: inferredPolicyCode }
609
+ : audit,
598
610
  };
599
611
  }
600
612
  export function parseCodexToolEvent(event) {
@@ -60,6 +60,63 @@ function normalizeAgent(item) {
60
60
  baseagent: typeof item?.baseagent === 'string' ? item.baseagent : undefined,
61
61
  };
62
62
  }
63
+ function channelTypeFromKey(channelKey) {
64
+ const value = typeof channelKey === 'string' ? channelKey.trim() : '';
65
+ if (!value)
66
+ return '';
67
+ const separator = value.indexOf('#');
68
+ return separator > 0 ? value.slice(0, separator) : value;
69
+ }
70
+ function triggerScheduleView(definition, fallback) {
71
+ const source = definition?.source ?? fallback?.source;
72
+ if (source && typeof source === 'object') {
73
+ switch (source.type) {
74
+ case 'once': return { scheduleType: 'once', scheduleValue: '' };
75
+ case 'delay': return { scheduleType: 'delay', scheduleValue: String(source.afterMs ?? '') };
76
+ case 'at': return { scheduleType: 'at', scheduleValue: String(source.at ?? '') };
77
+ case 'cron': return { scheduleType: 'cron', scheduleValue: String(source.expression ?? '') };
78
+ case 'interval': return { scheduleType: 'interval', scheduleValue: String(source.everyMs ?? '') };
79
+ case 'event': return { scheduleType: 'event', scheduleValue: String(source.eventPattern ?? '') };
80
+ default: break;
81
+ }
82
+ }
83
+ return {
84
+ scheduleType: String(fallback?.scheduleType ?? ''),
85
+ scheduleValue: String(fallback?.scheduleValue ?? ''),
86
+ };
87
+ }
88
+ function triggerDisplayFields(definition, fallback) {
89
+ const feedback = definition?.feedback ?? fallback?.feedback;
90
+ const origin = definition?.origin;
91
+ const target = feedback?.strategy === 'target'
92
+ ? (feedback.target ?? fallback?.feedback?.target)
93
+ : feedback?.strategy === 'origin'
94
+ ? {
95
+ channelKey: origin?.channelKey,
96
+ channelId: origin?.channelId ?? origin?.peerId,
97
+ channelType: origin?.channelType,
98
+ }
99
+ : null;
100
+ const schedule = triggerScheduleView(definition, fallback);
101
+ const targetChannel = target?.channelKey ?? fallback?.targetChannel ?? '';
102
+ const targetChannelType = target?.channelType
103
+ ?? fallback?.targetChannelType
104
+ ?? (feedback?.strategy === 'target'
105
+ ? channelTypeFromKey(targetChannel)
106
+ : (origin?.channelType ?? channelTypeFromKey(targetChannel)));
107
+ return {
108
+ scheduleType: schedule.scheduleType,
109
+ scheduleValue: schedule.scheduleValue,
110
+ targetChannel,
111
+ targetChannelName: targetChannel || fallback?.targetChannelName || '',
112
+ targetChannelId: target?.channelId ?? fallback?.targetChannelId ?? '',
113
+ targetChannelType,
114
+ createdByPeerId: origin?.peerId ?? fallback?.createdByPeerId ?? '',
115
+ createdByChannel: origin?.channelKey ?? fallback?.createdByChannel ?? '',
116
+ createdAt: definition?.createdAt ?? fallback?.createdAt,
117
+ updatedAt: definition?.updatedAt ?? fallback?.updatedAt,
118
+ };
119
+ }
63
120
  async function listTriggersForAgent(agentAid, agentLabel) {
64
121
  const p = resolvePaths();
65
122
  const controlToken = readTriggerControlToken(p.instanceDir);
@@ -106,6 +163,7 @@ async function listTriggersForAgent(agentAid, agentLabel) {
106
163
  revision,
107
164
  definitionError,
108
165
  subscription,
166
+ ...triggerDisplayFields(definition, trigger),
109
167
  };
110
168
  }));
111
169
  }