ui: gate machine access modes on api keys page

This commit is contained in:
a.tolmachev
2026-05-03 18:59:17 +00:00
parent 46f8e6cbeb
commit 076cead491
6 changed files with 93 additions and 1 deletions
+1 -1
View File
@@ -42,8 +42,8 @@ Progress:
- stable DTO и Community stub endpoints для `POST /mcp-auth/v1/token` и `POST /mcp-auth/v1/token/one-time`
- structured `403 forbidden` contract с `edition`, `machine_access_mode` и `upgrade_required`
- verification seam в `mcp-server` для bearer-token credentials без private Community implementation
- capability-gated `API Keys` surface показывает Community static-key flow и честно помечает commercial token modes как edition-locked
- pending:
- capability-gated UI/API surface для premium machine access
- final separation of Community `standard` flow from commercial `elevated/strict`
## Planned
+21
View File
@@ -145,6 +145,27 @@
</div>
</div>
<div class="section-card">
<div class="section-card-header">
<div>
<div class="section-card-title" id="machine-access-title" data-i18n="apikeys.machine_access.title">Machine access modes</div>
<div class="section-card-subtitle" id="machine-access-subtitle" data-i18n="apikeys.machine_access.subtitle">This build exposes the stable machine-access contract and shows which modes are available now.</div>
</div>
</div>
<div class="section-card-body" style="display:grid;gap:10px;">
<div style="font-size:13px;color:var(--text-secondary);line-height:1.65;" id="machine-access-summary" data-testid="machine-access-summary">
Community currently supports static AI-agent keys.
</div>
<div class="callout info">
<svg class="callout-icon" width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="var(--blue)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<circle cx="8" cy="8" r="6.5"/>
<path d="M8 11V8M8 5.5V5"/>
</svg>
<div id="machine-access-note" data-i18n="apikeys.machine_access.note">Short-lived and one-time token issuance uses the same public HTTP surface, but requires a commercial edition.</div>
</div>
</div>
</div>
<div class="section-card">
<div class="section-card-header">
<div>
+48
View File
@@ -1,5 +1,6 @@
var KEYS = [];
var AGENTS = [];
var capabilities = null;
var currentWorkspaceId = null;
var currentAgentId = null;
var selectedScopes = new Set(['read']);
@@ -20,6 +21,10 @@ function tfKey(key, vars) {
return window.tf ? tf(key, vars) : tKey(key);
}
function capabilityLabel(key) {
return tKey('settings.capability.' + key);
}
function buildIconSvg(href, width, height) {
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('width', String(width));
@@ -109,6 +114,47 @@ async function loadKeys() {
}
}
async function loadCapabilities() {
if (!window.CrankApi || typeof window.CrankApi.getCapabilities !== 'function') {
return;
}
try {
capabilities = await window.CrankApi.getCapabilities();
} catch (_error) {
capabilities = null;
}
renderMachineAccessSummary();
}
function renderMachineAccessSummary() {
var title = document.getElementById('machine-access-title');
var subtitle = document.getElementById('machine-access-subtitle');
var summary = document.getElementById('machine-access-summary');
var note = document.getElementById('machine-access-note');
if (!title || !subtitle || !summary || !note) {
return;
}
if (!capabilities) {
summary.textContent = tKey('apikeys.machine_access.summary_default');
note.textContent = tKey('apikeys.machine_access.note');
return;
}
var accessModes = (capabilities.machine_access_modes || []).map(capabilityLabel).join(', ');
var securityLevels = (capabilities.supported_security_levels || []).map(capabilityLabel).join(', ');
title.textContent = tKey('apikeys.machine_access.title_' + capabilities.edition);
subtitle.textContent = tKey('apikeys.machine_access.subtitle_' + capabilities.edition);
summary.textContent = tfKey('apikeys.machine_access.summary', {
access_modes: accessModes || capabilityLabel('none'),
security_levels: securityLevels || capabilityLabel('none'),
});
note.textContent = tKey('apikeys.machine_access.note_' + capabilities.edition);
}
function setTableLoading(on) {
var tbody = document.getElementById('keys-tbody');
if (!on) return;
@@ -444,8 +490,10 @@ function copyPrefix(prefix) {
document.addEventListener('DOMContentLoaded', async function() {
await (window.whenWorkspacesReady ? window.whenWorkspacesReady() : Promise.resolve());
await loadCapabilities();
await loadKeys();
window.addEventListener('crank:workspacechange', function() {
loadCapabilities();
loadKeys();
});
});
+16
View File
@@ -106,6 +106,14 @@ var TRANSLATIONS = {
'apikeys.agent.empty_hint': 'Create an AI agent first, then issue machine access keys for its MCP endpoint.',
'apikeys.agent.endpoint': 'MCP endpoint: {endpoint}',
'apikeys.agent.endpoint_missing': 'The selected agent does not have a published MCP endpoint yet.',
'apikeys.machine_access.title': 'Machine access modes',
'apikeys.machine_access.title_community': 'Community machine access',
'apikeys.machine_access.subtitle': 'This build exposes the stable machine-access contract and shows which modes are available now.',
'apikeys.machine_access.subtitle_community': 'Community exposes the stable MCP machine-access contract, but only static AI-agent keys are active in this build.',
'apikeys.machine_access.summary_default': 'Community currently supports static AI-agent keys.',
'apikeys.machine_access.summary': 'This build supports {access_modes} for operations marked with {security_levels}. Short-lived and one-time token issuance keeps the same public HTTP surface when a commercial edition is enabled.',
'apikeys.machine_access.note': 'Short-lived and one-time token issuance uses the same public HTTP surface, but requires a commercial edition.',
'apikeys.machine_access.note_community': 'Use static AI-agent keys in Community. Short-lived tokens and one-time tokens require a commercial edition.',
'apikeys.th.name': 'NAME',
'apikeys.th.scope': 'SCOPE',
'apikeys.th.created':'CREATED',
@@ -1159,6 +1167,14 @@ var TRANSLATIONS = {
'apikeys.agent.empty_hint': 'Сначала создайте AI-агента, затем выпустите машинный ключ для его MCP endpoint.',
'apikeys.agent.endpoint': 'MCP endpoint: {endpoint}',
'apikeys.agent.endpoint_missing': 'У выбранного агента пока нет опубликованного MCP endpoint.',
'apikeys.machine_access.title': 'Режимы машинного доступа',
'apikeys.machine_access.title_community': 'Машинный доступ Community',
'apikeys.machine_access.subtitle': 'Эта сборка показывает стабильный контракт машинного доступа и сразу отражает доступные режимы.',
'apikeys.machine_access.subtitle_community': 'Community публикует стабильный MCP-контракт машинного доступа, но в этой сборке активны только статические ключи AI-агентов.',
'apikeys.machine_access.summary_default': 'Community сейчас поддерживает только статические ключи AI-агентов.',
'apikeys.machine_access.summary': 'Эта сборка поддерживает {access_modes} для операций с уровнями защиты {security_levels}. При включении коммерческой редакции short-lived и one-time token issuance используют тот же public HTTP surface.',
'apikeys.machine_access.note': 'Выдача short-lived и one-time token использует тот же public HTTP surface, но требует коммерческую редакцию.',
'apikeys.machine_access.note_community': 'В Community используйте статические ключи AI-агентов. Short-lived и one-time token требуют коммерческую редакцию.',
'apikeys.th.name': 'НАЗВАНИЕ',
'apikeys.th.scope': 'ОБЛАСТЬ',
'apikeys.th.created':'СОЗДАН',
+6
View File
@@ -13,6 +13,12 @@ test('api keys page opens create key flow', async ({ page }) => {
});
await page.goto('/api-keys');
await expect(page.locator('.page-title')).toHaveText(localized('Agent Keys', 'Ключи агентов'));
await expect(page.locator('[data-testid="machine-access-summary"]')).toContainText(
localized('static AI-agent keys', 'статические ключи AI-агентов')
);
await expect(page.locator('#machine-access-note')).toContainText(
localized('commercial edition', 'коммерческую редакцию')
);
await expect(page.locator('#agent-select')).not.toBeDisabled();
await expect(page.locator('#btn-create-key')).toBeEnabled();
await page.locator('#btn-create-key').click();
+1
View File
@@ -47,6 +47,7 @@ UI должен честно отражать различия редакций:
- список ключей должен уметь показывать привязку к AI-агенту;
- модалка создания ключа должна позволять выбрать режим и область применения;
- copy должен объяснять, зачем нужен agent-scoped key и чем он отличается от будущих токенных режимов.
- `API Keys` page должна подтягивать `GET /api/admin/capabilities` и честно показывать, какие machine access modes доступны в текущей сборке, не притворяясь, что commercial token issuance уже работает в Community.
Основные файлы: