auth: cut over community machine access to agent keys
This commit is contained in:
+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',
|
||||
|
||||
Reference in New Issue
Block a user