ec-web 0.0.22 → 0.0.24
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 +5 -0
- package/dist/server.js +71 -160
- package/dist/sources/baseagent-detector.js +27 -102
- package/dist/sources/basic-settings.js +27 -4
- package/dist/sources/models.js +13 -6
- package/dist/sources/role-definitions.js +37 -12
- package/dist/static/app.js +100 -36
- package/dist/static/basic-settings.js +81 -14
- package/dist/static/index.html +1 -1
- package/package.json +1 -1
|
@@ -176,6 +176,10 @@ function baseagentPath(fieldPath) {
|
|
|
176
176
|
return null;
|
|
177
177
|
return { baseagent: match[1], field: match[2] };
|
|
178
178
|
}
|
|
179
|
+
function baseagentBlockPath(fieldPath) {
|
|
180
|
+
const match = fieldPath.match(/^baseagents\.([a-z0-9_-]+)$/);
|
|
181
|
+
return match?.[1] || null;
|
|
182
|
+
}
|
|
179
183
|
export function isAllowedBasicSettingsPath(file, fieldPath) {
|
|
180
184
|
try {
|
|
181
185
|
pathSegments(fieldPath);
|
|
@@ -187,6 +191,9 @@ export function isAllowedBasicSettingsPath(file, fieldPath) {
|
|
|
187
191
|
return DAEMON_EXACT_PATHS.has(fieldPath);
|
|
188
192
|
if (DEFAULTS_EXACT_PATHS.has(fieldPath))
|
|
189
193
|
return true;
|
|
194
|
+
const block = baseagentBlockPath(fieldPath);
|
|
195
|
+
if (block)
|
|
196
|
+
return BASEAGENT_SET.has(block);
|
|
190
197
|
const parsed = baseagentPath(fieldPath);
|
|
191
198
|
return !!parsed && BASEAGENT_SET.has(parsed.baseagent) && BASEAGENT_FIELDS[parsed.baseagent]?.has(parsed.field);
|
|
192
199
|
}
|
|
@@ -450,6 +457,11 @@ function validateDefaultsValue(fieldPath, value) {
|
|
|
450
457
|
const parsed = baseagentPath(fieldPath);
|
|
451
458
|
if (parsed)
|
|
452
459
|
return validateBaseagentValue(parsed.baseagent, parsed.field, fieldPath, value);
|
|
460
|
+
if (baseagentBlockPath(fieldPath)) {
|
|
461
|
+
if (!isRecord(value) || Object.keys(value).length > 0)
|
|
462
|
+
throw new Error(`${fieldPath} must be an empty object`);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
453
465
|
if (fieldPath === 'active_baseagent') {
|
|
454
466
|
if (!BASEAGENT_SET.has(String(value)))
|
|
455
467
|
throw new Error(`${fieldPath} is invalid`);
|
|
@@ -510,8 +522,7 @@ export function validateBasicSettingsChange(file, change) {
|
|
|
510
522
|
else
|
|
511
523
|
validateDefaultsValue(change.path, change.value);
|
|
512
524
|
}
|
|
513
|
-
function validateBaseagentAvailability(
|
|
514
|
-
const details = detectBaseAgentDetails(config);
|
|
525
|
+
function validateBaseagentAvailability(changes, details) {
|
|
515
526
|
const detected = new Set(details.filter(item => item.detected).map(item => item.id));
|
|
516
527
|
const configured = new Set(details.filter(item => item.configured).map(item => item.id));
|
|
517
528
|
for (const change of changes) {
|
|
@@ -522,6 +533,10 @@ function validateBaseagentAvailability(config, changes) {
|
|
|
522
533
|
if (parsed && !detected.has(parsed.baseagent) && !configured.has(parsed.baseagent)) {
|
|
523
534
|
throw Object.assign(new Error(`Base Agent is not available on this machine: ${parsed.baseagent}`), { statusCode: 400 });
|
|
524
535
|
}
|
|
536
|
+
const block = baseagentBlockPath(change.path);
|
|
537
|
+
if (block && change.op === 'set' && !detected.has(block) && !configured.has(block)) {
|
|
538
|
+
throw Object.assign(new Error(`Base Agent is not available on this machine: ${block}`), { statusCode: 400 });
|
|
539
|
+
}
|
|
525
540
|
}
|
|
526
541
|
}
|
|
527
542
|
function setAtPath(root, segments, value) {
|
|
@@ -675,7 +690,7 @@ async function readSnapshot(options = {}) {
|
|
|
675
690
|
},
|
|
676
691
|
secrets,
|
|
677
692
|
secretRefs,
|
|
678
|
-
baseagents: detectBaseAgentDetails(rawDefaults),
|
|
693
|
+
baseagents: await detectBaseAgentDetails(rawDefaults),
|
|
679
694
|
logTypes: logTypeStats.types,
|
|
680
695
|
logTypeCounts: logTypeStats.counts,
|
|
681
696
|
};
|
|
@@ -752,6 +767,9 @@ export async function handleBasicSettingsApi(req, res, options = {}) {
|
|
|
752
767
|
return;
|
|
753
768
|
}
|
|
754
769
|
if (req.method === 'GET' && pathname === '/api/basic-settings/secret') {
|
|
770
|
+
if (options.remoteAccess) {
|
|
771
|
+
throw Object.assign(new Error('Remote access cannot reveal configuration secrets'), { statusCode: 403 });
|
|
772
|
+
}
|
|
755
773
|
const file = url.searchParams.get('file') || 'defaults';
|
|
756
774
|
const fieldPath = url.searchParams.get('path') || '';
|
|
757
775
|
if (!requestedSecretPath(file, fieldPath)) {
|
|
@@ -767,6 +785,9 @@ export async function handleBasicSettingsApi(req, res, options = {}) {
|
|
|
767
785
|
return;
|
|
768
786
|
}
|
|
769
787
|
if (req.method === 'PATCH' && pathname === '/api/basic-settings') {
|
|
788
|
+
if (options.remoteAccess) {
|
|
789
|
+
throw Object.assign(new Error('Remote access cannot mutate basic settings'), { statusCode: 403 });
|
|
790
|
+
}
|
|
770
791
|
const body = await readJsonBody(req);
|
|
771
792
|
if (!isRecord(body) || (body.file !== 'daemon' && body.file !== 'defaults'))
|
|
772
793
|
throw new Error('file must be daemon or defaults');
|
|
@@ -779,6 +800,8 @@ export async function handleBasicSettingsApi(req, res, options = {}) {
|
|
|
779
800
|
changes.forEach(change => validateBasicSettingsChange(file, change));
|
|
780
801
|
const modules = await getConfigManager();
|
|
781
802
|
const target = file === 'daemon' ? modules.ConfigTarget.Process : modules.ConfigTarget.Defaults;
|
|
803
|
+
const currentConfig = modules.read(target, undefined, { cache: false, expand: false }) ?? {};
|
|
804
|
+
const detection = file === 'defaults' ? await detectBaseAgentDetails(currentConfig) : [];
|
|
782
805
|
const mutation = modules.mutateConfig(target, undefined, config => {
|
|
783
806
|
if (basicSettingsRevision(config) !== body.revision) {
|
|
784
807
|
throw Object.assign(new Error('Configuration changed after it was loaded. Reload and try again.'), {
|
|
@@ -786,7 +809,7 @@ export async function handleBasicSettingsApi(req, res, options = {}) {
|
|
|
786
809
|
});
|
|
787
810
|
}
|
|
788
811
|
if (file === 'defaults')
|
|
789
|
-
validateBaseagentAvailability(
|
|
812
|
+
validateBaseagentAvailability(changes, detection);
|
|
790
813
|
applyBasicSettingsChanges(config, file, changes);
|
|
791
814
|
const errors = modules.validateConfig(target, config);
|
|
792
815
|
if (errors.length > 0) {
|
package/dist/sources/models.js
CHANGED
|
@@ -32,21 +32,27 @@ export function normalizeModelEntries(models) {
|
|
|
32
32
|
owned_by: model.owned_by,
|
|
33
33
|
created: model.created,
|
|
34
34
|
family: inferModelFamily(id),
|
|
35
|
-
status: isAlias ? 'alias' : '
|
|
35
|
+
status: isAlias ? 'alias' : model.verification || 'unverified',
|
|
36
|
+
verification: model.verification,
|
|
37
|
+
reasoningLevels: model.reasoningLevels,
|
|
38
|
+
aliasFor: model.aliasFor,
|
|
36
39
|
isAlias
|
|
37
40
|
});
|
|
38
41
|
}
|
|
39
42
|
return out;
|
|
40
43
|
}
|
|
41
|
-
export async function getModelCatalogSnapshot(baseagent = 'claude') {
|
|
44
|
+
export async function getModelCatalogSnapshot(baseagent = 'claude', self, refresh = false) {
|
|
42
45
|
const { getCatalog } = await getCatalogModule();
|
|
43
|
-
const catalog = await getCatalog(
|
|
46
|
+
const catalog = await getCatalog(self, baseagent, refresh);
|
|
44
47
|
const normalized = normalizeModelEntries(catalog.models || []);
|
|
45
48
|
return {
|
|
46
49
|
models: normalized,
|
|
47
50
|
catalogModels: normalized,
|
|
48
51
|
source: catalog.source || 'unknown',
|
|
49
|
-
lastUpdate:
|
|
52
|
+
lastUpdate: catalog.fetchedAt,
|
|
53
|
+
revision: catalog.revision,
|
|
54
|
+
freshness: catalog.freshness,
|
|
55
|
+
diagnostics: catalog.diagnostics,
|
|
50
56
|
};
|
|
51
57
|
}
|
|
52
58
|
function sendJson(res, status, payload) {
|
|
@@ -58,11 +64,12 @@ export async function handleModelsApi(req, res) {
|
|
|
58
64
|
const url = new URL(req.url || '', 'http://localhost');
|
|
59
65
|
if (req.method === 'GET' && url.pathname === '/api/models/catalog') {
|
|
60
66
|
const baseagent = url.searchParams.get('baseagent') || 'claude';
|
|
61
|
-
if (
|
|
67
|
+
if (!['claude', 'codex', 'ecagent', 'gemini'].includes(baseagent)) {
|
|
62
68
|
sendJson(res, 400, { success: false, error: 'Invalid baseagent' });
|
|
63
69
|
return;
|
|
64
70
|
}
|
|
65
|
-
const
|
|
71
|
+
const self = url.searchParams.get('aid') || url.searchParams.get('self') || undefined;
|
|
72
|
+
const data = await getModelCatalogSnapshot(baseagent, self, url.searchParams.get('refresh') === 'true');
|
|
66
73
|
sendJson(res, 200, { success: true, data });
|
|
67
74
|
return;
|
|
68
75
|
}
|
|
@@ -278,7 +278,7 @@ function resolveRequestedAid(raw, agents) {
|
|
|
278
278
|
return agents.find(agent => agent?.aid)?.aid || null;
|
|
279
279
|
}
|
|
280
280
|
function isReservedRoleName(roleName) {
|
|
281
|
-
return roleName === 'owner' || roleName === 'admin';
|
|
281
|
+
return roleName === 'owner' || roleName === 'admin' || roleName === 'daemon-owner';
|
|
282
282
|
}
|
|
283
283
|
function isValidUserRoleName(roleName) {
|
|
284
284
|
return /^[a-z0-9_-]+$/.test(roleName) && !isReservedRoleName(roleName);
|
|
@@ -309,11 +309,12 @@ function normalizeDefaultRoleInput(value, current) {
|
|
|
309
309
|
return value;
|
|
310
310
|
return current ?? null;
|
|
311
311
|
}
|
|
312
|
-
function validateRoleDefinitionName(roleName) {
|
|
312
|
+
function validateRoleDefinitionName(roleName, options = {}) {
|
|
313
313
|
if (!roleName || !/^[a-z0-9_-]+$/.test(roleName))
|
|
314
314
|
return 'Invalid role name';
|
|
315
|
-
if (isReservedRoleName(roleName))
|
|
316
|
-
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
|
+
}
|
|
317
318
|
return null;
|
|
318
319
|
}
|
|
319
320
|
function inferSelectionMode(allowedModels = []) {
|
|
@@ -557,12 +558,12 @@ async function filterAllowedCatalogModels(models, allowedModels) {
|
|
|
557
558
|
const { isModelAllowedByPatterns } = await getRoleConstraintsModule();
|
|
558
559
|
return models.filter(model => !model.isAlias && isModelAllowedByPatterns(model.id, allowedModels));
|
|
559
560
|
}
|
|
560
|
-
async function buildModelPermissionResponse(roleName, roleDef, override) {
|
|
561
|
+
async function buildModelPermissionResponse(roleName, roleDef, override, aid) {
|
|
561
562
|
const currentPerm = getModelPermission(roleDef);
|
|
562
563
|
const defaultModel = override?.defaultModel ?? currentPerm.default ?? '';
|
|
563
564
|
const allowOverride = override?.allowOverride ?? currentPerm.allowOverride ?? false;
|
|
564
565
|
const allowedModels = override?.allowedModels ?? (Array.isArray(currentPerm.allowedModels) ? currentPerm.allowedModels : []);
|
|
565
|
-
const catalog = await getModelCatalogSnapshot('claude');
|
|
566
|
+
const catalog = await getModelCatalogSnapshot('claude', aid);
|
|
566
567
|
const matchingModels = await filterAllowedCatalogModels(catalog.models, allowedModels);
|
|
567
568
|
return {
|
|
568
569
|
role: roleName,
|
|
@@ -653,6 +654,7 @@ export async function handleRoleDefinitionsApi(req, res, auth = {}) {
|
|
|
653
654
|
const aid = resolveRequestedAid(parsedUrl.query.aid, agents);
|
|
654
655
|
const modules = await getRolesModule();
|
|
655
656
|
const { readRolesConfig, read, ConfigTarget } = modules;
|
|
657
|
+
let authorizationDecision = null;
|
|
656
658
|
const roleRoute = urlPath.match(/^\/api\/role-definitions\/([^/]+)$/);
|
|
657
659
|
const roleNestedRoute = urlPath.match(/^\/api\/role-definitions\/([^/]+)\/([^/]+)$/);
|
|
658
660
|
const decodeRoleName = (raw) => {
|
|
@@ -679,6 +681,7 @@ export async function handleRoleDefinitionsApi(req, res, auth = {}) {
|
|
|
679
681
|
method: req.method,
|
|
680
682
|
path: urlPath,
|
|
681
683
|
});
|
|
684
|
+
authorizationDecision = decision;
|
|
682
685
|
if (!decision.allow) {
|
|
683
686
|
console.warn('[role-definitions] Role policy access forbidden:', {
|
|
684
687
|
method: req.method,
|
|
@@ -724,13 +727,33 @@ export async function handleRoleDefinitionsApi(req, res, auth = {}) {
|
|
|
724
727
|
try {
|
|
725
728
|
const incoming = await readJsonBody(req);
|
|
726
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
|
+
}
|
|
727
743
|
if (incoming.roles && typeof incoming.roles === 'object') {
|
|
728
744
|
for (const [roleName, roleDef] of Object.entries(incoming.roles)) {
|
|
729
|
-
|
|
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 });
|
|
730
749
|
if (nameError) {
|
|
731
750
|
sendJson(res, 400, { error: `${nameError}: ${roleName}` });
|
|
732
751
|
return;
|
|
733
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
|
+
}
|
|
734
757
|
const validation = await validateRoleModelPermission(roleDef);
|
|
735
758
|
if (!validation.ok) {
|
|
736
759
|
sendJson(res, 400, { error: `Invalid model permissions for role ${roleName}`, errors: validation.errors });
|
|
@@ -842,7 +865,7 @@ export async function handleRoleDefinitionsApi(req, res, auth = {}) {
|
|
|
842
865
|
sendJson(res, 404, { success: false, error: 'Role not found' });
|
|
843
866
|
return;
|
|
844
867
|
}
|
|
845
|
-
const data = await buildModelPermissionResponse(roleName, roleDef);
|
|
868
|
+
const data = await buildModelPermissionResponse(roleName, roleDef, undefined, aid || undefined);
|
|
846
869
|
sendJson(res, 200, { success: true, data });
|
|
847
870
|
return;
|
|
848
871
|
}
|
|
@@ -861,7 +884,7 @@ export async function handleRoleDefinitionsApi(req, res, auth = {}) {
|
|
|
861
884
|
sendJson(res, 400, { success: false, error: 'Invalid model permissions', errors: validation.errors });
|
|
862
885
|
return;
|
|
863
886
|
}
|
|
864
|
-
const data = await buildModelPermissionResponse(roleName, roleDef, validation.value);
|
|
887
|
+
const data = await buildModelPermissionResponse(roleName, roleDef, validation.value, aid || undefined);
|
|
865
888
|
sendJson(res, 200, { success: true, data });
|
|
866
889
|
return;
|
|
867
890
|
}
|
|
@@ -891,13 +914,13 @@ export async function handleRoleDefinitionsApi(req, res, auth = {}) {
|
|
|
891
914
|
allowedModels: validation.value.allowedModels,
|
|
892
915
|
};
|
|
893
916
|
modules.roleService.writeRoleDefinition(aid, roleName, roleDef);
|
|
894
|
-
const data = await buildModelPermissionResponse(roleName, roleDef, validation.value);
|
|
917
|
+
const data = await buildModelPermissionResponse(roleName, roleDef, validation.value, aid || undefined);
|
|
895
918
|
sendJson(res, 200, { success: true, data });
|
|
896
919
|
return;
|
|
897
920
|
}
|
|
898
921
|
if (req.method === 'POST' && roleNestedRoute?.[2] === 'reset') {
|
|
899
922
|
const roleName = decodeRoleName(roleNestedRoute[1]);
|
|
900
|
-
if (!['admin', 'member', 'visitor'].includes(roleName)) {
|
|
923
|
+
if (!['owner', 'admin', 'member', 'visitor'].includes(roleName)) {
|
|
901
924
|
sendJson(res, 403, { error: 'Only builtin role overrides can be reset' });
|
|
902
925
|
return;
|
|
903
926
|
}
|
|
@@ -940,7 +963,9 @@ export async function handleRoleDefinitionsApi(req, res, auth = {}) {
|
|
|
940
963
|
try {
|
|
941
964
|
const roleName = decodeRoleName(roleRoute[1]);
|
|
942
965
|
const updates = await readJsonBody(req);
|
|
943
|
-
|
|
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 });
|
|
944
969
|
if (nameError) {
|
|
945
970
|
sendJson(res, 400, { error: nameError });
|
|
946
971
|
return;
|
package/dist/static/app.js
CHANGED
|
@@ -1976,6 +1976,58 @@ async function tryLocalAutoPair() {
|
|
|
1976
1976
|
return false;
|
|
1977
1977
|
}
|
|
1978
1978
|
|
|
1979
|
+
let authRecoveryPromise = null;
|
|
1980
|
+
|
|
1981
|
+
function authenticatedRequestUrl(pathOrUrl) {
|
|
1982
|
+
const raw = String(pathOrUrl || '');
|
|
1983
|
+
const target = raw.startsWith(BASE) ? raw : apiUrl(raw);
|
|
1984
|
+
const parsed = new URL(target, location.origin);
|
|
1985
|
+
const token = localStorage.getItem(TOKEN_KEY);
|
|
1986
|
+
if (token) parsed.searchParams.set('token', token);
|
|
1987
|
+
else parsed.searchParams.delete('token');
|
|
1988
|
+
return parsed.pathname + parsed.search + parsed.hash;
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
function authenticatedRequestInit(init = {}) {
|
|
1992
|
+
const headers = new Headers(init.headers || {});
|
|
1993
|
+
const token = localStorage.getItem(TOKEN_KEY);
|
|
1994
|
+
if (token) headers.set('Authorization', `Bearer ${token}`);
|
|
1995
|
+
else headers.delete('Authorization');
|
|
1996
|
+
return { ...init, headers };
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
async function recoverHttpAuthentication() {
|
|
2000
|
+
if (authRecoveryPromise) return authRecoveryPromise;
|
|
2001
|
+
localStorage.removeItem(TOKEN_KEY);
|
|
2002
|
+
authRecoveryPromise = (async () => {
|
|
2003
|
+
if (await tryLocalAutoPair()) return true;
|
|
2004
|
+
showPairPage(t('pair.error.tokenInvalid'));
|
|
2005
|
+
return false;
|
|
2006
|
+
})().finally(() => {
|
|
2007
|
+
authRecoveryPromise = null;
|
|
2008
|
+
});
|
|
2009
|
+
return authRecoveryPromise;
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
async function authFetch(pathOrUrl, init = {}) {
|
|
2013
|
+
const send = async () => {
|
|
2014
|
+
const token = localStorage.getItem(TOKEN_KEY);
|
|
2015
|
+
const response = await fetch(authenticatedRequestUrl(pathOrUrl), authenticatedRequestInit(init));
|
|
2016
|
+
return { response, token };
|
|
2017
|
+
};
|
|
2018
|
+
|
|
2019
|
+
let attempt = await send();
|
|
2020
|
+
if (attempt.response.status !== 401) return attempt.response;
|
|
2021
|
+
|
|
2022
|
+
const currentToken = localStorage.getItem(TOKEN_KEY);
|
|
2023
|
+
if (currentToken && currentToken !== attempt.token) {
|
|
2024
|
+
return (await send()).response;
|
|
2025
|
+
}
|
|
2026
|
+
if (!(await recoverHttpAuthentication())) return attempt.response;
|
|
2027
|
+
attempt = await send();
|
|
2028
|
+
return attempt.response;
|
|
2029
|
+
}
|
|
2030
|
+
|
|
1979
2031
|
function showPairPage(hint) {
|
|
1980
2032
|
if (ws) { try { ws.close(); } catch {} ws = null; }
|
|
1981
2033
|
$('#pair-page').style.display = 'flex';
|
|
@@ -4138,12 +4190,7 @@ function agentOpNew() {
|
|
|
4138
4190
|
openAgentCreateDrawer();
|
|
4139
4191
|
}
|
|
4140
4192
|
|
|
4141
|
-
const AGENT_BASEAGENT_TYPES = ['claude', 'codex', 'gemini'];
|
|
4142
|
-
const AGENT_EFFORT_FALLBACK = {
|
|
4143
|
-
claude: ['low', 'medium', 'high', 'xhigh', 'max'],
|
|
4144
|
-
codex: ['low', 'medium', 'high'],
|
|
4145
|
-
gemini: [],
|
|
4146
|
-
};
|
|
4193
|
+
const AGENT_BASEAGENT_TYPES = ['claude', 'codex', 'gemini', 'ecagent'];
|
|
4147
4194
|
const AGENT_AID_RE = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?){2,}$/;
|
|
4148
4195
|
let _agentEdit = null;
|
|
4149
4196
|
let _agentCreate = null;
|
|
@@ -4594,7 +4641,7 @@ async function agentFetchCreateModelOptions(baseagent) {
|
|
|
4594
4641
|
const catalog = await fetchModelCatalogData(baseagent);
|
|
4595
4642
|
return (catalog?.models || []).map(model => ({
|
|
4596
4643
|
value: model.id,
|
|
4597
|
-
label: model.id
|
|
4644
|
+
label: `${model.id}${model.verification === 'unverified' ? ' [unverified]' : ''}`,
|
|
4598
4645
|
}));
|
|
4599
4646
|
}
|
|
4600
4647
|
|
|
@@ -4664,8 +4711,17 @@ async function syncAgentCreateOptionFields(ctx, values) {
|
|
|
4664
4711
|
setAgentSelectLoading(modelSelect, currentModel, currentModel ? `${currentModel} (${t('common.loadingSuffix')})` : t('common.loading'));
|
|
4665
4712
|
setAgentSelectLoading(effortSelect, currentEffort, currentEffort ? `${currentEffort} (${t('common.loadingSuffix')})` : t('common.loading'));
|
|
4666
4713
|
|
|
4714
|
+
let catalog;
|
|
4667
4715
|
try {
|
|
4668
|
-
|
|
4716
|
+
// Load the catalog once for both selectors. The catalog endpoint is
|
|
4717
|
+
// connection-scoped and may perform a live gateway refresh, so issuing a
|
|
4718
|
+
// second identical request for effort metadata is both wasteful and can
|
|
4719
|
+
// produce mismatched model/effort options during a refresh.
|
|
4720
|
+
catalog = await fetchModelCatalogData(baseagent);
|
|
4721
|
+
const models = (catalog?.models || []).map(model => ({
|
|
4722
|
+
value: model.id,
|
|
4723
|
+
label: `${model.id}${model.verification === 'unverified' ? ' [unverified]' : ''}`,
|
|
4724
|
+
}));
|
|
4669
4725
|
if (_agentCreate !== ctx || ctx.optionLoadToken !== token) return;
|
|
4670
4726
|
modelSelect.innerHTML = agentSelectOptions(models, currentModel, t('common.defaultModel'));
|
|
4671
4727
|
} catch (e) {
|
|
@@ -4677,14 +4733,22 @@ async function syncAgentCreateOptionFields(ctx, values) {
|
|
|
4677
4733
|
if (_agentCreate === ctx && ctx.optionLoadToken === token) modelSelect.disabled = Boolean(ctx.saving);
|
|
4678
4734
|
}
|
|
4679
4735
|
|
|
4680
|
-
|
|
4681
|
-
|
|
4736
|
+
// A freshly rendered select may not expose its browser-selected value until
|
|
4737
|
+
// the next layout tick (and lightweight embedders may not implement the DOM
|
|
4738
|
+
// value reflection at all). Use the first catalog model as the default so
|
|
4739
|
+
// its reasoning levels are available immediately.
|
|
4740
|
+
const selectedModel = modelSelect?.value || currentModel || catalog?.models?.[0]?.id || '';
|
|
4741
|
+
const efforts = catalog?.models?.find(entry => entry.id === selectedModel)?.reasoningLevels || [];
|
|
4742
|
+
if (_agentCreate !== ctx || ctx.optionLoadToken !== token) return;
|
|
4743
|
+
effortSelect.innerHTML = agentSelectOptions(efforts, currentEffort, t('common.default'));
|
|
4744
|
+
effortSelect.disabled = Boolean(ctx.saving);
|
|
4682
4745
|
}
|
|
4683
4746
|
|
|
4684
4747
|
async function syncAgentCreateEffortOptions(ctx, baseagent, model, currentEffort, token) {
|
|
4685
4748
|
const effortSelect = ctx.backdrop.querySelector('#ag-create-effort');
|
|
4686
4749
|
if (!effortSelect) return;
|
|
4687
|
-
const
|
|
4750
|
+
const catalog = await fetchModelCatalogData(baseagent);
|
|
4751
|
+
const efforts = catalog.models.find(entry => entry.id === model)?.reasoningLevels || [];
|
|
4688
4752
|
if (_agentCreate !== ctx || ctx.optionLoadToken !== token) return;
|
|
4689
4753
|
effortSelect.innerHTML = agentSelectOptions(efforts, currentEffort, t('common.default'));
|
|
4690
4754
|
effortSelect.disabled = Boolean(ctx.saving);
|
|
@@ -5085,12 +5149,11 @@ async function syncAgentEffortOptions(ctx, baseagent, model, currentEffort, toke
|
|
|
5085
5149
|
setAgentSelectLoading(effortSelect, currentEffort, currentEffort ? `${currentEffort} (${t('common.loadingSuffix')})` : t('common.loading'));
|
|
5086
5150
|
try {
|
|
5087
5151
|
let efforts = await agentFetchOptions('effort', ctx.aid, { baseagent, model, effort: currentEffort || 'auto' });
|
|
5088
|
-
if (!efforts.length && AGENT_EFFORT_FALLBACK[baseagent]) efforts = AGENT_EFFORT_FALLBACK[baseagent];
|
|
5089
5152
|
if (_agentEdit !== ctx || ctx.optionLoadToken !== token) return;
|
|
5090
5153
|
effortSelect.innerHTML = agentSelectOptions(efforts, currentEffort, t('common.default'));
|
|
5091
5154
|
} catch (e) {
|
|
5092
5155
|
if (_agentEdit === ctx && ctx.optionLoadToken === token) {
|
|
5093
|
-
const fallback =
|
|
5156
|
+
const fallback = [];
|
|
5094
5157
|
effortSelect.innerHTML = agentSelectOptions(fallback, currentEffort, t('common.default'));
|
|
5095
5158
|
toast(e.message || String(e), true);
|
|
5096
5159
|
}
|
|
@@ -6883,12 +6946,11 @@ async function syncTriggerEffortOptions(ctx, baseagent, model, currentEffort, to
|
|
|
6883
6946
|
setAgentSelectLoading(effortSelect, currentEffort, currentEffort ? `${currentEffort} (${t('common.loadingSuffix')})` : t('common.loading'));
|
|
6884
6947
|
try {
|
|
6885
6948
|
let efforts = await agentFetchOptions('effort', ctx.original.agentAid, { baseagent, model, effort: currentEffort || 'auto' });
|
|
6886
|
-
if (!efforts.length && AGENT_EFFORT_FALLBACK[baseagent]) efforts = AGENT_EFFORT_FALLBACK[baseagent];
|
|
6887
6949
|
if (_triggerEdit !== ctx || ctx.optionLoadToken !== token) return;
|
|
6888
6950
|
effortSelect.innerHTML = agentSelectOptions(efforts, currentEffort, t('common.inherited'));
|
|
6889
6951
|
} catch (e) {
|
|
6890
6952
|
if (_triggerEdit === ctx && ctx.optionLoadToken === token) {
|
|
6891
|
-
const fallback =
|
|
6953
|
+
const fallback = [];
|
|
6892
6954
|
effortSelect.innerHTML = agentSelectOptions(fallback, currentEffort, t('common.inherited'));
|
|
6893
6955
|
toast(e.message || String(e), true);
|
|
6894
6956
|
}
|
|
@@ -7811,7 +7873,7 @@ async function loadUsageOverview(rangeType, customFrom, customTo) {
|
|
|
7811
7873
|
if (fromTs) params.set('from', String(fromTs));
|
|
7812
7874
|
if (toTs) params.set('to', String(toTs));
|
|
7813
7875
|
|
|
7814
|
-
const resp = await
|
|
7876
|
+
const resp = await authFetch('api/stats/overview?' + params.toString(), {
|
|
7815
7877
|
headers: { Authorization: 'Bearer ' + localStorage.getItem(TOKEN_KEY) }
|
|
7816
7878
|
});
|
|
7817
7879
|
data = resp.ok ? await resp.json() : null;
|
|
@@ -8016,7 +8078,7 @@ function initDetailQuery() {
|
|
|
8016
8078
|
|
|
8017
8079
|
async function loadDetailAgentList() {
|
|
8018
8080
|
try {
|
|
8019
|
-
const resp = await
|
|
8081
|
+
const resp = await authFetch('api/stats/agents', {
|
|
8020
8082
|
headers: { Authorization: 'Bearer ' + localStorage.getItem(TOKEN_KEY) }
|
|
8021
8083
|
});
|
|
8022
8084
|
if (!resp.ok) return;
|
|
@@ -8056,7 +8118,7 @@ async function loadDetailModelList() {
|
|
|
8056
8118
|
if (timeRange.fromTs) params.set('from', String(timeRange.fromTs));
|
|
8057
8119
|
if (timeRange.toTs) params.set('to', String(timeRange.toTs));
|
|
8058
8120
|
|
|
8059
|
-
const resp = await
|
|
8121
|
+
const resp = await authFetch('api/stats/models?' + params.toString(), {
|
|
8060
8122
|
headers: { Authorization: 'Bearer ' + localStorage.getItem(TOKEN_KEY) }
|
|
8061
8123
|
});
|
|
8062
8124
|
if (!resp.ok) return;
|
|
@@ -8109,7 +8171,7 @@ async function queryDetailUsage() {
|
|
|
8109
8171
|
params.set('offset', String(offset));
|
|
8110
8172
|
|
|
8111
8173
|
try {
|
|
8112
|
-
const resp = await
|
|
8174
|
+
const resp = await authFetch('api/stats/detail?' + params.toString(), {
|
|
8113
8175
|
headers: { Authorization: 'Bearer ' + localStorage.getItem(TOKEN_KEY) }
|
|
8114
8176
|
});
|
|
8115
8177
|
if (seq !== _detailQuerySeq) return; // 已有更新的查询发出,丢弃本次结果
|
|
@@ -8430,7 +8492,7 @@ async function loadExplorerModels() {
|
|
|
8430
8492
|
if (_expTimeRange.toTs) params.set('to', String(_expTimeRange.toTs));
|
|
8431
8493
|
|
|
8432
8494
|
try {
|
|
8433
|
-
const resp = await
|
|
8495
|
+
const resp = await authFetch('api/stats/models?' + params.toString(), {
|
|
8434
8496
|
headers: { Authorization: 'Bearer ' + localStorage.getItem(TOKEN_KEY) }
|
|
8435
8497
|
});
|
|
8436
8498
|
if (!resp.ok) return;
|
|
@@ -8467,7 +8529,7 @@ async function fetchExplorerOverviewData(filterParams) {
|
|
|
8467
8529
|
if (filterParams.peer) params.set('peer', filterParams.peer);
|
|
8468
8530
|
}
|
|
8469
8531
|
|
|
8470
|
-
const resp = await
|
|
8532
|
+
const resp = await authFetch('api/stats/overview?' + params.toString(), {
|
|
8471
8533
|
headers: { Authorization: 'Bearer ' + localStorage.getItem(TOKEN_KEY) }
|
|
8472
8534
|
});
|
|
8473
8535
|
const data = resp.ok ? await resp.json() : null;
|
|
@@ -8499,7 +8561,7 @@ async function loadExplorerSidebar() {
|
|
|
8499
8561
|
var token = localStorage.getItem(TOKEN_KEY);
|
|
8500
8562
|
var headers = { Authorization: 'Bearer ' + token };
|
|
8501
8563
|
try {
|
|
8502
|
-
var agentsResp = await
|
|
8564
|
+
var agentsResp = await authFetch('api/stats/agents', { headers });
|
|
8503
8565
|
var agents = agentsResp.ok ? await agentsResp.json() : [];
|
|
8504
8566
|
renderExplorerAgentList(agents);
|
|
8505
8567
|
|
|
@@ -8560,7 +8622,7 @@ async function loadPeersForAgent(agentAid) {
|
|
|
8560
8622
|
params.set('agent', agentAid);
|
|
8561
8623
|
// 不传递时间范围,获取该 agent 的所有 peers
|
|
8562
8624
|
|
|
8563
|
-
var resp = await
|
|
8625
|
+
var resp = await authFetch('api/stats/peers?' + params.toString(), { headers });
|
|
8564
8626
|
var peers = resp.ok ? await resp.json() : [];
|
|
8565
8627
|
renderExplorerPeerList(peers);
|
|
8566
8628
|
} catch {
|
|
@@ -8642,7 +8704,7 @@ async function runExplorerQuery() {
|
|
|
8642
8704
|
|
|
8643
8705
|
var data;
|
|
8644
8706
|
try {
|
|
8645
|
-
var resp = await
|
|
8707
|
+
var resp = await authFetch('api/stats/explorer?' + params.toString(), {
|
|
8646
8708
|
headers: { Authorization: 'Bearer ' + localStorage.getItem(TOKEN_KEY) }
|
|
8647
8709
|
});
|
|
8648
8710
|
if (!resp.ok) return;
|
|
@@ -9430,7 +9492,7 @@ async function updatePeerRoleOverride(agentAid, scope, targetId, role, groupId,
|
|
|
9430
9492
|
if (peerId) body.peerId = peerId;
|
|
9431
9493
|
if (role) body.role = role;
|
|
9432
9494
|
|
|
9433
|
-
const res = await
|
|
9495
|
+
const res = await authFetch(`api/assignments/peer/${encodeURIComponent(agentAid)}/${encodeURIComponent(targetId)}`, {
|
|
9434
9496
|
method: role ? 'PUT' : 'DELETE',
|
|
9435
9497
|
headers: {
|
|
9436
9498
|
'Content-Type': 'application/json',
|
|
@@ -9725,7 +9787,7 @@ function renderDefaultRoleSelector(data) {
|
|
|
9725
9787
|
const next = { private: privateSelect.value || null, group: groupSelect.value || null };
|
|
9726
9788
|
try {
|
|
9727
9789
|
const token = localStorage.getItem(TOKEN_KEY);
|
|
9728
|
-
const writeRes = await
|
|
9790
|
+
const writeRes = await authFetch(roleDefinitionsApi('api/role-definitions'), {
|
|
9729
9791
|
method: 'PUT',
|
|
9730
9792
|
headers: {
|
|
9731
9793
|
'Content-Type': 'application/json',
|
|
@@ -9793,7 +9855,7 @@ function formatCommandPermissions(commandPermissions) {
|
|
|
9793
9855
|
async function fetchRoleOperationsCatalog() {
|
|
9794
9856
|
if (roleOperationsCache) return roleOperationsCache;
|
|
9795
9857
|
const token = localStorage.getItem(TOKEN_KEY);
|
|
9796
|
-
const res = await
|
|
9858
|
+
const res = await authFetch('api/role-definitions/operations', {
|
|
9797
9859
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
9798
9860
|
});
|
|
9799
9861
|
if (!res.ok) throw new Error('Failed to load CLI operation catalog');
|
|
@@ -10315,8 +10377,9 @@ function createRoleCard(roleName, roleDef) {
|
|
|
10315
10377
|
const model = roleDef.permissions?.['baseagents.claude.model']?.default || '—';
|
|
10316
10378
|
const commandStats = getCommandPermissionStats(roleDef.commandPermissions);
|
|
10317
10379
|
|
|
10318
|
-
//
|
|
10319
|
-
|
|
10380
|
+
// Management identities and built-in user roles are both protected from
|
|
10381
|
+
// deletion; their local policy overrides can be reset instead.
|
|
10382
|
+
const builtinRoles = ['owner', 'admin', 'member', 'visitor'];
|
|
10320
10383
|
const isBuiltin = builtinRoles.includes(roleName);
|
|
10321
10384
|
|
|
10322
10385
|
card.innerHTML = `
|
|
@@ -10402,7 +10465,7 @@ function initRoleDefinitionsTab() {
|
|
|
10402
10465
|
|
|
10403
10466
|
try {
|
|
10404
10467
|
const token = localStorage.getItem(TOKEN_KEY);
|
|
10405
|
-
const res = await
|
|
10468
|
+
const res = await authFetch(roleDefinitionsApi(`api/role-definitions/${roleName}/reset`), {
|
|
10406
10469
|
method: 'POST',
|
|
10407
10470
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
10408
10471
|
});
|
|
@@ -10447,7 +10510,7 @@ function initRoleDefinitionsTab() {
|
|
|
10447
10510
|
: replacement
|
|
10448
10511
|
? `?replacementRoleId=${encodeURIComponent(replacement)}`
|
|
10449
10512
|
: '';
|
|
10450
|
-
const res = await
|
|
10513
|
+
const res = await authFetch(roleDefinitionsApi(`api/role-definitions/${roleName}${replacementQuery}`), {
|
|
10451
10514
|
method: 'DELETE',
|
|
10452
10515
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
10453
10516
|
});
|
|
@@ -10600,7 +10663,7 @@ function renderRoleModelPermissionSection(roleDef) {
|
|
|
10600
10663
|
|
|
10601
10664
|
async function fetchRoleModelPermissionData(roleName) {
|
|
10602
10665
|
const token = localStorage.getItem(TOKEN_KEY);
|
|
10603
|
-
const res = await
|
|
10666
|
+
const res = await authFetch(roleDefinitionsApi(`api/role-definitions/${encodeURIComponent(roleName)}/configurable-models`), {
|
|
10604
10667
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
10605
10668
|
});
|
|
10606
10669
|
if (!res.ok) throw new Error('Failed to load role model permissions');
|
|
@@ -10608,13 +10671,14 @@ async function fetchRoleModelPermissionData(roleName) {
|
|
|
10608
10671
|
return json.data;
|
|
10609
10672
|
}
|
|
10610
10673
|
|
|
10611
|
-
async function fetchModelCatalogData(baseagent = 'claude') {
|
|
10674
|
+
async function fetchModelCatalogData(baseagent = 'claude', aid = '') {
|
|
10612
10675
|
const token = localStorage.getItem(TOKEN_KEY);
|
|
10613
|
-
const res = await
|
|
10676
|
+
const res = await authFetch(`api/models/catalog?baseagent=${encodeURIComponent(baseagent)}${aid ? `&aid=${encodeURIComponent(aid)}` : ''}`, {
|
|
10614
10677
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
10615
10678
|
});
|
|
10616
10679
|
if (!res.ok) throw new Error('Failed to load model catalog');
|
|
10617
10680
|
const json = await res.json();
|
|
10681
|
+
if (json.data?.diagnostics?.some(d => d.code === 'AUTH_FAILED')) throw new Error('Model gateway authentication failed');
|
|
10618
10682
|
return json.data;
|
|
10619
10683
|
}
|
|
10620
10684
|
|
|
@@ -10777,7 +10841,7 @@ async function initRoleModelPermissionEditor(container, roleName, roleDef) {
|
|
|
10777
10841
|
try {
|
|
10778
10842
|
const data = roleName && roleName !== '__new__'
|
|
10779
10843
|
? await fetchRoleModelPermissionData(roleName)
|
|
10780
|
-
: await fetchModelCatalogData();
|
|
10844
|
+
: await fetchModelCatalogData('claude', roleDefinitionsCurrentAgent || state.roleDefinitions?.aid || '');
|
|
10781
10845
|
catalogModels = data?.catalog?.models || data?.models || [];
|
|
10782
10846
|
} catch (err) {
|
|
10783
10847
|
console.warn('[roleDefinitions] Failed to load model catalog:', err);
|
|
@@ -11574,7 +11638,7 @@ async function saveRoleDefinition() {
|
|
|
11574
11638
|
const method = isNew ? 'POST' : 'PUT';
|
|
11575
11639
|
const url = isNew ? roleDefinitionsApi('api/role-definitions') : roleDefinitionsApi(`api/role-definitions/${encodeURIComponent(actualRoleName)}`);
|
|
11576
11640
|
|
|
11577
|
-
const res = await
|
|
11641
|
+
const res = await authFetch(url, {
|
|
11578
11642
|
method,
|
|
11579
11643
|
headers: {
|
|
11580
11644
|
'Content-Type': 'application/json',
|