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.
- package/dist/index.js +4 -2
- package/dist/ipc-client.js +12 -1
- package/dist/paths.js +18 -1
- package/dist/server.js +111 -154
- package/dist/sources/baseagent-detector.js +27 -102
- package/dist/sources/basic-settings.js +21 -4
- package/dist/sources/models.js +13 -6
- package/dist/sources/monitor.js +22 -5
- package/dist/sources/relation-roles.js +67 -17
- package/dist/sources/role-definitions.js +64 -15
- package/dist/sources/session-codex.js +18 -6
- package/dist/sources/triggers.js +58 -0
- package/dist/static/app.js +173 -39
- package/dist/static/basic-settings.js +81 -14
- package/dist/static/index.html +1 -1
- package/package.json +1 -1
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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) {
|
package/dist/sources/triggers.js
CHANGED
|
@@ -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
|
}
|