auth: cut over community machine access to agent keys
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
<base href="../">
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Crank — API Keys</title>
|
||||
<title>Crank — Agent Keys</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="css/variables.css">
|
||||
<link rel="stylesheet" href="css/layout.css">
|
||||
@@ -107,8 +107,8 @@
|
||||
|
||||
<div class="page-header">
|
||||
<div class="page-header-text">
|
||||
<h1 class="page-title" data-i18n="apikeys.title">API Keys</h1>
|
||||
<p class="page-subtitle" data-i18n="apikeys.subtitle">Keys authenticate external MCP clients and workspace automation against Crank.</p>
|
||||
<h1 class="page-title" data-i18n="apikeys.title">Agent Keys</h1>
|
||||
<p class="page-subtitle" data-i18n="apikeys.subtitle">Keys authenticate external MCP clients against a specific AI agent endpoint.</p>
|
||||
</div>
|
||||
<div class="page-header-actions">
|
||||
<button class="btn-primary" id="btn-create-key" type="button">
|
||||
@@ -129,11 +129,27 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-card">
|
||||
<div class="section-card-header">
|
||||
<div>
|
||||
<div class="section-card-title" data-i18n="apikeys.agent.title">Agent access</div>
|
||||
<div class="section-card-subtitle" id="agent-access-subtitle" data-i18n="apikeys.agent.subtitle">Select the AI agent whose MCP endpoint should accept this key.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-card-body" style="display:grid;gap:12px;">
|
||||
<div class="field-group" style="margin-bottom:0;">
|
||||
<label class="field-label" for="agent-select" data-i18n="apikeys.agent.label">AI agent</label>
|
||||
<select class="field-input" id="agent-select"></select>
|
||||
<div class="field-hint" id="agent-endpoint-hint" data-testid="agent-key-endpoint-hint"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-card">
|
||||
<div class="section-card-header">
|
||||
<div>
|
||||
<div class="section-card-title" data-i18n="apikeys.active.title">Active keys</div>
|
||||
<div class="section-card-subtitle">3 active · 1 revoked</div>
|
||||
<div class="section-card-subtitle" id="keys-summary-subtitle">3 active · 1 revoked</div>
|
||||
</div>
|
||||
<div class="filter-bar-search" style="max-width: 220px; margin: 0;">
|
||||
<svg width="13" height="13" style="position:absolute; left:9px; top:50%; transform:translateY(-50%); color:var(--text-muted);" viewBox="0 0 16 16" fill="currentColor"><path d="M10.68 11.74a6 6 0 01-7.922-8.982 6 6 0 018.982 7.922l3.04 3.04a.749.749 0 01-.326 1.275.749.749 0 01-.734-.215zM11.5 7a4.499 4.499 0 11-8.997 0A4.499 4.499 0 0111.5 7z"/></svg>
|
||||
@@ -192,7 +208,7 @@
|
||||
<div class="modal-overlay" id="modal-create">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<span class="modal-title" data-i18n="apikeys.modal.title">Create API key</span>
|
||||
<span class="modal-title" data-i18n="apikeys.modal.title">Create agent key</span>
|
||||
<button class="modal-close" id="modal-close-btn" type="button">
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round">
|
||||
<line x1="1" y1="1" x2="11" y2="11"/><line x1="11" y1="1" x2="1" y2="11"/>
|
||||
|
||||
+101
-8
@@ -1,5 +1,7 @@
|
||||
var KEYS = [];
|
||||
var AGENTS = [];
|
||||
var currentWorkspaceId = null;
|
||||
var currentAgentId = null;
|
||||
var selectedScopes = new Set(['read']);
|
||||
var search = '';
|
||||
|
||||
@@ -32,6 +34,7 @@ function mapKeyRecord(record) {
|
||||
var apiKey = record.api_key || {};
|
||||
return {
|
||||
id: apiKey.id,
|
||||
agentId: apiKey.agent_id || null,
|
||||
name: apiKey.name,
|
||||
prefix: apiKey.prefix || '',
|
||||
scopes: apiKey.scopes || [],
|
||||
@@ -41,6 +44,15 @@ function mapKeyRecord(record) {
|
||||
};
|
||||
}
|
||||
|
||||
function mapAgentRecord(record) {
|
||||
return {
|
||||
id: record.id,
|
||||
slug: record.slug,
|
||||
displayName: record.display_name || record.slug || record.id,
|
||||
mcpEndpoint: record.mcp_endpoint || '',
|
||||
};
|
||||
}
|
||||
|
||||
function currentWorkspace() {
|
||||
return window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
|
||||
}
|
||||
@@ -52,19 +64,46 @@ async function loadKeys() {
|
||||
setTableLoading(true);
|
||||
|
||||
if (!currentWorkspaceId || !window.CrankApi) {
|
||||
AGENTS = [];
|
||||
KEYS = [];
|
||||
currentAgentId = null;
|
||||
renderAgentPicker();
|
||||
setCreateButtonState();
|
||||
setTableLoading(false);
|
||||
renderTable(tKey('apikeys.error.api'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var response = await window.CrankApi.listPlatformApiKeys(currentWorkspaceId);
|
||||
KEYS = ((response && response.items) || []).map(mapKeyRecord);
|
||||
var response = await window.CrankApi.listAgents(currentWorkspaceId);
|
||||
AGENTS = ((response && response.items) || []).map(mapAgentRecord);
|
||||
if (!AGENTS.length) {
|
||||
currentAgentId = null;
|
||||
KEYS = [];
|
||||
renderAgentPicker();
|
||||
setCreateButtonState();
|
||||
setTableLoading(false);
|
||||
renderTable();
|
||||
return;
|
||||
}
|
||||
if (!currentAgentId || !AGENTS.some(function(agent) { return agent.id === currentAgentId; })) {
|
||||
currentAgentId = AGENTS[0].id;
|
||||
}
|
||||
renderAgentPicker();
|
||||
var keysResponse = await window.CrankApi.listAgentPlatformApiKeys(
|
||||
currentWorkspaceId,
|
||||
currentAgentId
|
||||
);
|
||||
KEYS = ((keysResponse && keysResponse.items) || []).map(mapKeyRecord);
|
||||
setCreateButtonState();
|
||||
setTableLoading(false);
|
||||
renderTable();
|
||||
} catch (error) {
|
||||
AGENTS = [];
|
||||
KEYS = [];
|
||||
currentAgentId = null;
|
||||
renderAgentPicker();
|
||||
setCreateButtonState();
|
||||
setTableLoading(false);
|
||||
renderTable(error.message || tKey('apikeys.error.load'));
|
||||
}
|
||||
@@ -85,10 +124,11 @@ function setTableLoading(on) {
|
||||
|
||||
async function revokeKey(id) {
|
||||
if (!confirm(tKey('apikeys.confirm.revoke'))) return;
|
||||
if (!currentAgentId) return;
|
||||
|
||||
try {
|
||||
var key = KEYS.find(function(item) { return item.id === id; });
|
||||
await window.CrankApi.revokePlatformApiKey(currentWorkspaceId, id);
|
||||
await window.CrankApi.revokeAgentPlatformApiKey(currentWorkspaceId, currentAgentId, id);
|
||||
await loadKeys();
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.success(
|
||||
@@ -108,10 +148,11 @@ async function revokeKey(id) {
|
||||
|
||||
async function deleteKey(id) {
|
||||
if (!confirm(tKey('apikeys.confirm.delete'))) return;
|
||||
if (!currentAgentId) return;
|
||||
|
||||
try {
|
||||
var key = KEYS.find(function(item) { return item.id === id; });
|
||||
await window.CrankApi.deletePlatformApiKey(currentWorkspaceId, id);
|
||||
await window.CrankApi.deleteAgentPlatformApiKey(currentWorkspaceId, currentAgentId, id);
|
||||
await loadKeys();
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.success(
|
||||
@@ -130,7 +171,7 @@ async function deleteKey(id) {
|
||||
}
|
||||
|
||||
async function createKey(name, scopes) {
|
||||
var created = await window.CrankApi.createPlatformApiKey(currentWorkspaceId, {
|
||||
var created = await window.CrankApi.createAgentPlatformApiKey(currentWorkspaceId, currentAgentId, {
|
||||
name: name,
|
||||
scopes: scopes,
|
||||
});
|
||||
@@ -140,6 +181,48 @@ async function createKey(name, scopes) {
|
||||
};
|
||||
}
|
||||
|
||||
function currentAgent() {
|
||||
return AGENTS.find(function(agent) { return agent.id === currentAgentId; }) || null;
|
||||
}
|
||||
|
||||
function renderAgentPicker() {
|
||||
var select = document.getElementById('agent-select');
|
||||
var hint = document.getElementById('agent-endpoint-hint');
|
||||
if (!select || !hint) return;
|
||||
|
||||
select.innerHTML = '';
|
||||
|
||||
if (!AGENTS.length) {
|
||||
var emptyOption = document.createElement('option');
|
||||
emptyOption.value = '';
|
||||
emptyOption.textContent = tKey('apikeys.agent.empty');
|
||||
select.appendChild(emptyOption);
|
||||
select.disabled = true;
|
||||
hint.textContent = tKey('apikeys.agent.empty_hint');
|
||||
return;
|
||||
}
|
||||
|
||||
AGENTS.forEach(function(agent) {
|
||||
var option = document.createElement('option');
|
||||
option.value = agent.id;
|
||||
option.textContent = agent.displayName;
|
||||
if (agent.id === currentAgentId) option.selected = true;
|
||||
select.appendChild(option);
|
||||
});
|
||||
select.disabled = false;
|
||||
|
||||
var agent = currentAgent();
|
||||
hint.textContent = agent && agent.mcpEndpoint
|
||||
? tfKey('apikeys.agent.endpoint', { endpoint: agent.mcpEndpoint })
|
||||
: tKey('apikeys.agent.endpoint_missing');
|
||||
}
|
||||
|
||||
function setCreateButtonState() {
|
||||
var button = document.getElementById('btn-create-key');
|
||||
if (!button) return;
|
||||
button.disabled = !currentAgentId;
|
||||
}
|
||||
|
||||
function renderScopes() {
|
||||
var el = document.getElementById('scope-checkboxes');
|
||||
var tmpl = document.getElementById('tmpl-scope-checkbox');
|
||||
@@ -165,14 +248,16 @@ function renderTable(errorMessage) {
|
||||
var rows = KEYS.filter(function(key) {
|
||||
return !q || key.name.toLowerCase().includes(q) || key.prefix.toLowerCase().includes(q);
|
||||
});
|
||||
var subtitle = document.querySelector('.section-card-subtitle');
|
||||
var subtitle = document.getElementById('keys-summary-subtitle');
|
||||
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (subtitle) {
|
||||
var active = KEYS.filter(function(key) { return key.status === 'active'; }).length;
|
||||
var revoked = KEYS.filter(function(key) { return key.status === 'revoked'; }).length;
|
||||
subtitle.textContent = tfKey('apikeys.active.subtitle', { active: active, revoked: revoked });
|
||||
subtitle.textContent = currentAgentId
|
||||
? tfKey('apikeys.active.subtitle', { active: active, revoked: revoked })
|
||||
: tKey('apikeys.agent.empty_hint');
|
||||
}
|
||||
|
||||
if (errorMessage) {
|
||||
@@ -191,7 +276,9 @@ function renderTable(errorMessage) {
|
||||
var td = document.createElement('td');
|
||||
td.colSpan = 7;
|
||||
td.style.cssText = 'text-align:center;padding:36px;color:var(--text-muted);';
|
||||
td.textContent = KEYS.length ? tKey('apikeys.empty.search') : tKey('apikeys.empty.none');
|
||||
td.textContent = currentAgentId
|
||||
? (KEYS.length ? tKey('apikeys.empty.search') : tKey('apikeys.empty.none'))
|
||||
: tKey('apikeys.agent.empty_hint');
|
||||
empty.appendChild(td);
|
||||
tbody.appendChild(empty);
|
||||
return;
|
||||
@@ -242,6 +329,7 @@ function renderTable(errorMessage) {
|
||||
var modal = document.getElementById('modal-create');
|
||||
|
||||
function openModal() {
|
||||
if (!currentAgentId) return;
|
||||
document.getElementById('modal-form-body').hidden = false;
|
||||
document.getElementById('modal-reveal-body').hidden = true;
|
||||
document.getElementById('modal-footer-create').hidden = false;
|
||||
@@ -340,6 +428,11 @@ document.getElementById('key-search').addEventListener('input', function() {
|
||||
renderTable();
|
||||
});
|
||||
|
||||
document.getElementById('agent-select').addEventListener('change', async function() {
|
||||
currentAgentId = this.value || null;
|
||||
await loadKeys();
|
||||
});
|
||||
|
||||
function copyPrefix(prefix) {
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(prefix).catch(function() {});
|
||||
|
||||
+8
-8
@@ -331,17 +331,17 @@
|
||||
archiveAgent: function(workspaceId, agentId) {
|
||||
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/agents/' + encodeURIComponent(agentId) + '/archive', {});
|
||||
},
|
||||
listPlatformApiKeys: function(workspaceId) {
|
||||
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/platform-api-keys');
|
||||
listAgentPlatformApiKeys: function(workspaceId, agentId) {
|
||||
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/agents/' + encodeURIComponent(agentId) + '/platform-api-keys');
|
||||
},
|
||||
createPlatformApiKey: function(workspaceId, payload) {
|
||||
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/platform-api-keys', payload);
|
||||
createAgentPlatformApiKey: function(workspaceId, agentId, payload) {
|
||||
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/agents/' + encodeURIComponent(agentId) + '/platform-api-keys', payload);
|
||||
},
|
||||
revokePlatformApiKey: function(workspaceId, keyId) {
|
||||
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/platform-api-keys/' + encodeURIComponent(keyId) + '/revoke', {});
|
||||
revokeAgentPlatformApiKey: function(workspaceId, agentId, keyId) {
|
||||
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/agents/' + encodeURIComponent(agentId) + '/platform-api-keys/' + encodeURIComponent(keyId) + '/revoke', {});
|
||||
},
|
||||
deletePlatformApiKey: function(workspaceId, keyId) {
|
||||
return del('/workspaces/' + encodeURIComponent(workspaceId) + '/platform-api-keys/' + encodeURIComponent(keyId));
|
||||
deleteAgentPlatformApiKey: function(workspaceId, agentId, keyId) {
|
||||
return del('/workspaces/' + encodeURIComponent(workspaceId) + '/agents/' + encodeURIComponent(agentId) + '/platform-api-keys/' + encodeURIComponent(keyId));
|
||||
},
|
||||
listSecrets: function(workspaceId) {
|
||||
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/secrets');
|
||||
|
||||
+30
-16
@@ -96,9 +96,16 @@ var TRANSLATIONS = {
|
||||
'page.ops': 'operations',
|
||||
|
||||
// API Keys page
|
||||
'apikeys.title': 'API Keys',
|
||||
'apikeys.subtitle': 'Keys authenticate external MCP clients and workspace automation against Crank.',
|
||||
'apikeys.title': 'Agent Keys',
|
||||
'apikeys.subtitle': 'Keys authenticate external MCP clients against a specific AI agent endpoint.',
|
||||
'apikeys.new': 'Create key',
|
||||
'apikeys.agent.title': 'Agent access',
|
||||
'apikeys.agent.subtitle': 'Select the AI agent whose MCP endpoint should accept this key.',
|
||||
'apikeys.agent.label': 'AI agent',
|
||||
'apikeys.agent.empty': 'No agents available',
|
||||
'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.th.name': 'NAME',
|
||||
'apikeys.th.scope': 'SCOPE',
|
||||
'apikeys.th.created':'CREATED',
|
||||
@@ -112,10 +119,10 @@ var TRANSLATIONS = {
|
||||
'apikeys.th.scopes': 'SCOPES',
|
||||
'apikeys.th.status': 'STATUS',
|
||||
'apikeys.scope_ref': 'Scope reference',
|
||||
'apikeys.scope.read': 'Initialize MCP sessions, ping the server, and list tools for a workspace agent.',
|
||||
'apikeys.scope.read': 'Initialize MCP sessions, ping the server, and list tools for the selected AI agent.',
|
||||
'apikeys.scope.write': 'Execute `tools/call` requests against published agent toolsets.',
|
||||
'apikeys.scope.deploy': 'Reserved for deploy-scoped automation. Today it also permits MCP read/write flows.',
|
||||
'apikeys.modal.title': 'Create API key',
|
||||
'apikeys.modal.title': 'Create agent key',
|
||||
'apikeys.modal.name': 'Key name',
|
||||
'apikeys.modal.name_hint': 'A descriptive label to identify the key. Only visible to admins.',
|
||||
'apikeys.modal.name_placeholder': 'e.g. Production, CI pipeline',
|
||||
@@ -146,11 +153,11 @@ var TRANSLATIONS = {
|
||||
'apikeys.toast.delete_error_message': 'Failed to delete key',
|
||||
'apikeys.toast.scope_missing_title': 'Missing scope',
|
||||
'apikeys.toast.scope_missing_message': 'Select at least one scope before creating a key.',
|
||||
'apikeys.toast.create_title': 'API key created',
|
||||
'apikeys.toast.create_title': 'Agent key created',
|
||||
'apikeys.toast.create_message': 'Copy the key now. It will not be shown again.',
|
||||
'apikeys.toast.create_error_title': 'Key creation failed',
|
||||
'apikeys.toast.create_error_message': 'Failed to create key',
|
||||
'apikeys.toast.copy_title': 'API key copied',
|
||||
'apikeys.toast.copy_title': 'Agent key copied',
|
||||
'apikeys.toast.copy_message': 'Store the raw key securely. It cannot be revealed again.',
|
||||
'apikeys.toast.prefix_title': 'Key prefix copied',
|
||||
'apikeys.action.copy_prefix': 'Copy key prefix',
|
||||
@@ -471,7 +478,7 @@ var TRANSLATIONS = {
|
||||
'workspace_setup.create.footer': "You'll be the Owner of this workspace. You can invite additional members after creation.",
|
||||
'workspace_setup.danger.title': 'Danger zone',
|
||||
'workspace_setup.danger.export_title': 'Export all data',
|
||||
'workspace_setup.danger.export_body': 'Download a JSON snapshot of workspace settings, memberships, invitations, operations, agents and platform API keys.',
|
||||
'workspace_setup.danger.export_body': 'Download a JSON snapshot of workspace settings, memberships, invitations, operations, agents and agent access keys.',
|
||||
'workspace_setup.danger.delete_title': 'Delete workspace',
|
||||
'workspace_setup.danger.delete_body': 'Permanently deletes all operations, keys, logs and settings. This action cannot be undone. You will be logged out immediately.',
|
||||
'workspace_setup.export': 'Export',
|
||||
@@ -927,7 +934,7 @@ var TRANSLATIONS = {
|
||||
'agents.empty.search.title': 'No agents match "{query}"',
|
||||
'agents.empty.search.sub': 'Try a different search term.',
|
||||
'agents.card.tools': 'tools',
|
||||
'agents.card.keys': 'API keys',
|
||||
'agents.card.keys': 'keys',
|
||||
'agents.card.calls_today': 'calls today',
|
||||
'agents.card.created': 'Created {date}',
|
||||
'agents.card.copy_endpoint': 'Copy endpoint',
|
||||
@@ -1112,9 +1119,16 @@ var TRANSLATIONS = {
|
||||
'page.ops': 'операций',
|
||||
|
||||
// API Keys page
|
||||
'apikeys.title': 'API Ключи',
|
||||
'apikeys.subtitle': 'Ключи аутентифицируют внешние MCP-клиенты и автоматизацию воркспейса в Crank.',
|
||||
'apikeys.title': 'Ключи агентов',
|
||||
'apikeys.subtitle': 'Ключи аутентифицируют внешние MCP-клиенты для конкретного endpoint AI-агента.',
|
||||
'apikeys.new': 'Создать ключ',
|
||||
'apikeys.agent.title': 'Доступ агента',
|
||||
'apikeys.agent.subtitle': 'Выберите AI-агента, чей MCP endpoint должен принимать этот ключ.',
|
||||
'apikeys.agent.label': 'AI-агент',
|
||||
'apikeys.agent.empty': 'Агенты отсутствуют',
|
||||
'apikeys.agent.empty_hint': 'Сначала создайте AI-агента, затем выпустите машинный ключ для его MCP endpoint.',
|
||||
'apikeys.agent.endpoint': 'MCP endpoint: {endpoint}',
|
||||
'apikeys.agent.endpoint_missing': 'У выбранного агента пока нет опубликованного MCP endpoint.',
|
||||
'apikeys.th.name': 'НАЗВАНИЕ',
|
||||
'apikeys.th.scope': 'ОБЛАСТЬ',
|
||||
'apikeys.th.created':'СОЗДАН',
|
||||
@@ -1128,10 +1142,10 @@ var TRANSLATIONS = {
|
||||
'apikeys.th.scopes': 'ОБЛАСТИ',
|
||||
'apikeys.th.status': 'СТАТУС',
|
||||
'apikeys.scope_ref': 'Справка по областям',
|
||||
'apikeys.scope.read': 'Инициализация MCP-сессий, ping сервера и получение списка инструментов для агента воркспейса.',
|
||||
'apikeys.scope.read': 'Инициализация MCP-сессий, ping сервера и получение списка инструментов для выбранного AI-агента.',
|
||||
'apikeys.scope.write': 'Выполнение `tools/call` для опубликованных наборов инструментов агента.',
|
||||
'apikeys.scope.deploy': 'Зарезервировано для deploy-автоматизации. Сейчас также разрешает MCP read/write сценарии.',
|
||||
'apikeys.modal.title': 'Создать API-ключ',
|
||||
'apikeys.modal.title': 'Создать ключ агента',
|
||||
'apikeys.modal.name': 'Имя ключа',
|
||||
'apikeys.modal.name_hint': 'Понятная метка для идентификации ключа. Видна только администраторам.',
|
||||
'apikeys.modal.name_placeholder': 'например, Production, CI pipeline',
|
||||
@@ -1162,11 +1176,11 @@ var TRANSLATIONS = {
|
||||
'apikeys.toast.delete_error_message': 'Не удалось удалить ключ',
|
||||
'apikeys.toast.scope_missing_title': 'Не выбрана область',
|
||||
'apikeys.toast.scope_missing_message': 'Выберите хотя бы одну область перед созданием ключа.',
|
||||
'apikeys.toast.create_title': 'API-ключ создан',
|
||||
'apikeys.toast.create_title': 'Ключ агента создан',
|
||||
'apikeys.toast.create_message': 'Скопируйте ключ сейчас. Повторно он не будет показан.',
|
||||
'apikeys.toast.create_error_title': 'Не удалось создать ключ',
|
||||
'apikeys.toast.create_error_message': 'Не удалось создать ключ',
|
||||
'apikeys.toast.copy_title': 'API-ключ скопирован',
|
||||
'apikeys.toast.copy_title': 'Ключ агента скопирован',
|
||||
'apikeys.toast.copy_message': 'Сохраните исходный ключ в надежном месте. Повторно показать его нельзя.',
|
||||
'apikeys.toast.prefix_title': 'Префикс ключа скопирован',
|
||||
'apikeys.action.copy_prefix': 'Скопировать префикс ключа',
|
||||
@@ -1493,7 +1507,7 @@ var TRANSLATIONS = {
|
||||
'workspace_setup.create.footer': 'Вы станете владельцем этого воркспейса. После создания можно пригласить дополнительных участников.',
|
||||
'workspace_setup.danger.title': 'Опасная зона',
|
||||
'workspace_setup.danger.export_title': 'Экспортировать все данные',
|
||||
'workspace_setup.danger.export_body': 'Скачать JSON-снимок настроек воркспейса, участников, приглашений, операций, агентов и platform API keys.',
|
||||
'workspace_setup.danger.export_body': 'Скачать JSON-снимок настроек воркспейса, участников, приглашений, операций, агентов и ключей доступа агентов.',
|
||||
'workspace_setup.danger.delete_title': 'Удалить воркспейс',
|
||||
'workspace_setup.danger.delete_body': 'Безвозвратно удаляет все операции, ключи, логи и настройки. Действие нельзя отменить. Вы будете немедленно разлогинены.',
|
||||
'workspace_setup.export': 'Экспорт',
|
||||
@@ -1957,7 +1971,7 @@ var TRANSLATIONS = {
|
||||
'agents.empty.search.title': 'Нет агентов по запросу "{query}"',
|
||||
'agents.empty.search.sub': 'Попробуйте другой поисковый запрос.',
|
||||
'agents.card.tools': 'инструментов',
|
||||
'agents.card.keys': 'API-ключей',
|
||||
'agents.card.keys': 'ключей',
|
||||
'agents.card.calls_today': 'вызовов сегодня',
|
||||
'agents.card.created': 'Создан {date}',
|
||||
'agents.card.copy_endpoint': 'Скопировать endpoint',
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const { login, localized } = require('./helpers');
|
||||
const { createAgent, getCurrentWorkspace, login, localized, uniqueName } = require('./helpers');
|
||||
|
||||
test('api keys page opens create key flow', async ({ page }) => {
|
||||
await login(page);
|
||||
const workspace = await getCurrentWorkspace(page);
|
||||
await createAgent(page, workspace.id, {
|
||||
slug: uniqueName('playwright_keys_agent'),
|
||||
display_name: 'Playwright Keys Agent',
|
||||
description: 'Agent for API key page smoke test.',
|
||||
instructions: {},
|
||||
tool_selection_policy: {},
|
||||
});
|
||||
await page.goto('/api-keys');
|
||||
await expect(page.locator('.page-title')).toHaveText(localized('API Keys', 'API Ключи'));
|
||||
await expect(page.locator('.page-title')).toHaveText(localized('Agent Keys', 'Ключи агентов'));
|
||||
await expect(page.locator('#agent-select')).not.toBeDisabled();
|
||||
await expect(page.locator('#btn-create-key')).toBeEnabled();
|
||||
await page.locator('#btn-create-key').click();
|
||||
await expect(page.locator('.modal-title')).toHaveText(localized('Create API key', 'Создать API-ключ'));
|
||||
await expect(page.locator('#modal-create')).toHaveClass(/open/);
|
||||
await expect(page.locator('.modal-title')).toHaveText(localized('Create agent key', 'Создать ключ агента'));
|
||||
await page.locator('#new-key-name').fill(`playwright-${Date.now()}`);
|
||||
await page.locator('#modal-confirm-btn').click();
|
||||
await expect(page.locator('#modal-reveal-body')).toContainText(localized('Copy this key now', 'Скопируйте этот ключ сейчас'));
|
||||
|
||||
@@ -87,11 +87,11 @@ async function publishOperation(page, workspaceId, operationId, version = 1) {
|
||||
);
|
||||
}
|
||||
|
||||
async function createPlatformApiKey(page, workspaceId, name, scopes) {
|
||||
async function createPlatformApiKey(page, workspaceId, agentId, name, scopes) {
|
||||
return browserJson(
|
||||
page,
|
||||
'POST',
|
||||
`/api/admin/workspaces/${encodeURIComponent(workspaceId)}/platform-api-keys`,
|
||||
`/api/admin/workspaces/${encodeURIComponent(workspaceId)}/agents/${encodeURIComponent(agentId)}/platform-api-keys`,
|
||||
{ name, scopes },
|
||||
);
|
||||
}
|
||||
@@ -520,7 +520,13 @@ async function setupPublishedAgent(page, { operationPayload, agentSlug, toolName
|
||||
]);
|
||||
await publishAgent(page, workspace.id, createdAgent.agent_id, createdAgent.version);
|
||||
|
||||
const key = await createPlatformApiKey(page, workspace.id, uniqueName('playwright_key'), ['read', 'write']);
|
||||
const key = await createPlatformApiKey(
|
||||
page,
|
||||
workspace.id,
|
||||
createdAgent.agent_id,
|
||||
uniqueName('playwright_key'),
|
||||
['read', 'write']
|
||||
);
|
||||
|
||||
return {
|
||||
workspace,
|
||||
|
||||
Reference in New Issue
Block a user