diff --git a/TASKS.md b/TASKS.md index 3d6b95c..6cda721 100644 --- a/TASKS.md +++ b/TASKS.md @@ -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 diff --git a/apps/ui/html/api-keys.html b/apps/ui/html/api-keys.html index 0a62a4d..646b193 100644 --- a/apps/ui/html/api-keys.html +++ b/apps/ui/html/api-keys.html @@ -145,6 +145,27 @@ +
+
+
+
Machine access modes
+
This build exposes the stable machine-access contract and shows which modes are available now.
+
+
+
+
+ Community currently supports static AI-agent keys. +
+
+ + + + +
Short-lived and one-time token issuance uses the same public HTTP surface, but requires a commercial edition.
+
+
+
+
diff --git a/apps/ui/js/api-keys.js b/apps/ui/js/api-keys.js index f4cf493..6f31241 100644 --- a/apps/ui/js/api-keys.js +++ b/apps/ui/js/api-keys.js @@ -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(); }); }); diff --git a/apps/ui/js/i18n.js b/apps/ui/js/i18n.js index fdae5d7..7bcc200 100644 --- a/apps/ui/js/i18n.js +++ b/apps/ui/js/i18n.js @@ -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':'СОЗДАН', diff --git a/apps/ui/tests/e2e/api-keys.spec.js b/apps/ui/tests/e2e/api-keys.spec.js index 262dbbf..8a86d2e 100644 --- a/apps/ui/tests/e2e/api-keys.spec.js +++ b/apps/ui/tests/e2e/api-keys.spec.js @@ -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(); diff --git a/docs/frontend-roadmap.md b/docs/frontend-roadmap.md index eef6442..b104ad0 100644 --- a/docs/frontend-roadmap.md +++ b/docs/frontend-roadmap.md @@ -47,6 +47,7 @@ UI должен честно отражать различия редакций: - список ключей должен уметь показывать привязку к AI-агенту; - модалка создания ключа должна позволять выбрать режим и область применения; - copy должен объяснять, зачем нужен agent-scoped key и чем он отличается от будущих токенных режимов. +- `API Keys` page должна подтягивать `GET /api/admin/capabilities` и честно показывать, какие machine access modes доступны в текущей сборке, не притворяясь, что commercial token issuance уже работает в Community. Основные файлы: