ui: gate machine access modes on api keys page
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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':'СОЗДАН',
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -47,6 +47,7 @@ UI должен честно отражать различия редакций:
|
||||
- список ключей должен уметь показывать привязку к AI-агенту;
|
||||
- модалка создания ключа должна позволять выбрать режим и область применения;
|
||||
- copy должен объяснять, зачем нужен agent-scoped key и чем он отличается от будущих токенных режимов.
|
||||
- `API Keys` page должна подтягивать `GET /api/admin/capabilities` и честно показывать, какие machine access modes доступны в текущей сборке, не притворяясь, что commercial token issuance уже работает в Community.
|
||||
|
||||
Основные файлы:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user