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.
@@ -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(config, changes) {
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(config, changes);
812
+ validateBaseagentAvailability(changes, detection);
790
813
  applyBasicSettingsChanges(config, file, changes);
791
814
  const errors = modules.validateConfig(target, config);
792
815
  if (errors.length > 0) {
@@ -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' : 'available',
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(undefined, baseagent);
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: new Date().toISOString()
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 (!/^[a-z0-9_-]+$/i.test(baseagent)) {
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 data = await getModelCatalogSnapshot(baseagent);
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
- 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 });
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
- 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 });
944
969
  if (nameError) {
945
970
  sendJson(res, 400, { error: nameError });
946
971
  return;
@@ -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
- const models = await agentFetchCreateModelOptions(baseagent);
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
- const selectedModel = modelSelect?.value || currentModel;
4681
- await syncAgentCreateEffortOptions(ctx, baseagent, selectedModel, currentEffort, token);
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 efforts = AGENT_EFFORT_FALLBACK[baseagent] || [];
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 = AGENT_EFFORT_FALLBACK[baseagent] || [];
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 = AGENT_EFFORT_FALLBACK[baseagent] || [];
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 fetch(authApiUrl('api/stats/overview?' + params.toString()), {
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 fetch(authApiUrl('api/stats/agents'), {
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 fetch(authApiUrl('api/stats/models?' + params.toString()), {
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 fetch(authApiUrl('api/stats/detail?' + params.toString()), {
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 fetch(authApiUrl('api/stats/models?' + params.toString()), {
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 fetch(authApiUrl('api/stats/overview?' + params.toString()), {
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 fetch(authApiUrl('api/stats/agents'), { headers });
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 fetch(authApiUrl('api/stats/peers?' + params.toString()), { headers });
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 fetch(authApiUrl('api/stats/explorer?' + params.toString()), {
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 fetch(authApiUrl(`api/assignments/peer/${encodeURIComponent(agentAid)}/${encodeURIComponent(targetId)}`), {
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 fetch(roleDefinitionsApi('api/role-definitions'), {
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 fetch(authApiUrl('api/role-definitions/operations'), {
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
- const builtinRoles = ['admin', 'member', 'visitor'];
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 fetch(roleDefinitionsApi(`api/role-definitions/${roleName}/reset`), {
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 fetch(roleDefinitionsApi(`api/role-definitions/${roleName}${replacementQuery}`), {
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 fetch(roleDefinitionsApi(`api/role-definitions/${encodeURIComponent(roleName)}/configurable-models`), {
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 fetch(authApiUrl(`api/models/catalog?baseagent=${encodeURIComponent(baseagent)}`), {
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 fetch(url, {
11641
+ const res = await authFetch(url, {
11578
11642
  method,
11579
11643
  headers: {
11580
11644
  'Content-Type': 'application/json',