api: expose community edition capabilities
This commit is contained in:
@@ -29,8 +29,9 @@ Implementation slices:
|
|||||||
4. Зафиксировать Community defaults в docs и runtime config.
|
4. Зафиксировать Community defaults в docs и runtime config.
|
||||||
|
|
||||||
Progress:
|
Progress:
|
||||||
- pending: define shared capability payload in `crank-core` and `admin-api`;
|
- done: shared capability payload exists in `crank-core` and `admin-api`;
|
||||||
- pending: expose Community capability defaults to UI;
|
- done: authenticated `/api/admin/capabilities` exposes Community defaults to UI;
|
||||||
|
- in_progress: first UI consumer uses capability payload in `Settings`;
|
||||||
- pending: gate premium protocols and security levels in wizard and agents flows.
|
- pending: gate premium protocols and security levels in wizard and agents flows.
|
||||||
|
|
||||||
DoD:
|
DoD:
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ use crate::{
|
|||||||
update_profile,
|
update_profile,
|
||||||
},
|
},
|
||||||
auth_profiles::{create_auth_profile, get_auth_profile, list_auth_profiles},
|
auth_profiles::{create_auth_profile, get_auth_profile, list_auth_profiles},
|
||||||
|
capabilities::get_capabilities,
|
||||||
observability::{get_agent_usage, get_log, get_operation_usage, get_usage, list_logs},
|
observability::{get_agent_usage, get_log, get_operation_usage, get_usage, list_logs},
|
||||||
operations::{
|
operations::{
|
||||||
archive_operation, create_operation, create_version, delete_operation,
|
archive_operation, create_operation, create_version, delete_operation,
|
||||||
@@ -167,6 +168,7 @@ pub fn build_app(state: AppState) -> Router {
|
|||||||
.route("/async-jobs/{job_id}/result", get(get_async_job_result));
|
.route("/async-jobs/{job_id}/result", get(get_async_job_result));
|
||||||
|
|
||||||
let workspace_root_router = Router::new()
|
let workspace_root_router = Router::new()
|
||||||
|
.route("/capabilities", get(get_capabilities))
|
||||||
.route("/workspaces", get(list_workspaces).post(create_workspace))
|
.route("/workspaces", get(list_workspaces).post(create_workspace))
|
||||||
.layer(middleware::from_fn_with_state(
|
.layer(middleware::from_fn_with_state(
|
||||||
state.clone(),
|
state.clone(),
|
||||||
@@ -999,6 +1001,43 @@ mod tests {
|
|||||||
assert_eq!(delete_status, reqwest::StatusCode::NO_CONTENT);
|
assert_eq!(delete_status, reqwest::StatusCode::NO_CONTENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
#[serial]
|
||||||
|
async fn returns_community_capabilities() {
|
||||||
|
let registry = test_registry().await;
|
||||||
|
let storage_root = test_storage_root("community_capabilities");
|
||||||
|
let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await;
|
||||||
|
let client = authorized_client(&base_url).await;
|
||||||
|
let root_url = base_url
|
||||||
|
.as_ref()
|
||||||
|
.split("/api/admin/workspaces/")
|
||||||
|
.next()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let response = assert_success_json(
|
||||||
|
client
|
||||||
|
.get(format!("{root_url}/api/admin/capabilities"))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(response["edition"], "community");
|
||||||
|
assert_eq!(
|
||||||
|
response["supported_protocols"],
|
||||||
|
json!(["rest", "graphql", "grpc"])
|
||||||
|
);
|
||||||
|
assert_eq!(response["supported_security_levels"], json!(["standard"]));
|
||||||
|
assert_eq!(
|
||||||
|
response["machine_access_modes"],
|
||||||
|
json!(["static_agent_key"])
|
||||||
|
);
|
||||||
|
assert_eq!(response["limits"]["max_workspaces"], 1);
|
||||||
|
assert_eq!(response["limits"]["max_users_per_workspace"], 1);
|
||||||
|
assert_eq!(response["limits"]["max_agents_per_workspace"], 1);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
#[serial]
|
#[serial]
|
||||||
async fn manages_workspace_access_lifecycle() {
|
async fn manages_workspace_access_lifecycle() {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ pub mod access;
|
|||||||
pub mod agents;
|
pub mod agents;
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod auth_profiles;
|
pub mod auth_profiles;
|
||||||
|
pub mod capabilities;
|
||||||
pub mod observability;
|
pub mod observability;
|
||||||
pub mod operations;
|
pub mod operations;
|
||||||
pub mod secrets;
|
pub mod secrets;
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
use axum::{Json, extract::State};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use crate::state::AppState;
|
||||||
|
|
||||||
|
pub async fn get_capabilities(State(state): State<AppState>) -> Json<serde_json::Value> {
|
||||||
|
Json(json!(state.service.get_capabilities().await))
|
||||||
|
}
|
||||||
@@ -9,13 +9,15 @@ use crank_adapter_grpc::test_support as grpc_test_support;
|
|||||||
use crank_adapter_soap::{SoapServiceSummary, inspect_wsdl};
|
use crank_adapter_soap::{SoapServiceSummary, inspect_wsdl};
|
||||||
use crank_core::{
|
use crank_core::{
|
||||||
Agent, AgentId, AgentOperationBinding, AgentStatus, AgentVersion, AggregationMode,
|
Agent, AgentId, AgentOperationBinding, AgentStatus, AgentVersion, AggregationMode,
|
||||||
AsyncJobHandle, AuthConfig, AuthKind, AuthProfile, AuthProfileId, ConfigExport, ExecutionMode,
|
AsyncJobHandle, AuthConfig, AuthKind, AuthProfile, AuthProfileId, ConfigExport,
|
||||||
ExportMode, GeneratedDraft, GeneratedDraftStatus, InvitationId, InvitationStatus,
|
EditionCapabilities, EditionLimits, ExecutionMode, ExportMode, GeneratedDraft,
|
||||||
InvitationToken, InvocationLevel, InvocationLog, InvocationLogId, InvocationSource,
|
GeneratedDraftStatus, InvitationId, InvitationStatus, InvitationToken, InvocationLevel,
|
||||||
InvocationStatus, JobStatus, MembershipRole, OperationId, OperationStatus, PlatformApiKey,
|
InvocationLog, InvocationLogId, InvocationSource, InvocationStatus, JobStatus,
|
||||||
PlatformApiKeyId, PlatformApiKeyScope, PlatformApiKeyStatus, Protocol, SampleId, Samples,
|
MachineAccessMode, MembershipRole, OperationId, OperationSecurityLevel, OperationStatus,
|
||||||
Secret, SecretId, SecretKind, SecretStatus, StreamSession, StreamStatus, Target,
|
PlatformApiKey, PlatformApiKeyId, PlatformApiKeyScope, PlatformApiKeyStatus, ProductEdition,
|
||||||
TransportBehavior, UsagePeriod, UserId, UserSessionId, Workspace, WorkspaceId, WorkspaceStatus,
|
Protocol, SampleId, Samples, Secret, SecretId, SecretKind, SecretStatus, StreamSession,
|
||||||
|
StreamStatus, Target, TransportBehavior, UsagePeriod, UserId, UserSessionId, Workspace,
|
||||||
|
WorkspaceId, WorkspaceStatus,
|
||||||
};
|
};
|
||||||
use crank_mapping::{JsonPathRoot, MappingSet, infer_mapping_from_samples};
|
use crank_mapping::{JsonPathRoot, MappingSet, infer_mapping_from_samples};
|
||||||
use crank_proto::{ProtoService, services_from_descriptor_set_bytes};
|
use crank_proto::{ProtoService, services_from_descriptor_set_bytes};
|
||||||
@@ -1566,6 +1568,20 @@ impl AdminService {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_capabilities(&self) -> EditionCapabilities {
|
||||||
|
EditionCapabilities {
|
||||||
|
edition: ProductEdition::Community,
|
||||||
|
supported_protocols: vec![Protocol::Rest, Protocol::Graphql, Protocol::Grpc],
|
||||||
|
supported_security_levels: vec![OperationSecurityLevel::Standard],
|
||||||
|
machine_access_modes: vec![MachineAccessMode::StaticAgentKey],
|
||||||
|
limits: EditionLimits {
|
||||||
|
max_workspaces: Some(1),
|
||||||
|
max_users_per_workspace: Some(1),
|
||||||
|
max_agents_per_workspace: Some(1),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip(self))]
|
||||||
pub async fn list_stream_sessions(
|
pub async fn list_stream_sessions(
|
||||||
&self,
|
&self,
|
||||||
|
|||||||
@@ -274,7 +274,7 @@
|
|||||||
<polygon points="8,1.5 15.5,14.5 0.5,14.5" fill="none"/><path d="M8 6v4M8 11.5v.5"/>
|
<polygon points="8,1.5 15.5,14.5 0.5,14.5" fill="none"/><path d="M8 6v4M8 11.5v.5"/>
|
||||||
</svg>
|
</svg>
|
||||||
<div>
|
<div>
|
||||||
<strong data-i18n="settings.security.not_available">Available later.</strong> <span data-i18n="settings.security.capabilities_body">Two-factor authentication, passkeys, and multi-session controls will be added later. The current live flow uses password sign-in with one HttpOnly browser session.</span>
|
<strong id="settings-security-capability-title" data-i18n="settings.security.not_available">Available later.</strong> <span id="settings-security-capability-body" data-i18n="settings.security.capabilities_body">Two-factor authentication, passkeys, and multi-session controls will be added later. The current live flow uses password sign-in with one HttpOnly browser session.</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -172,6 +172,9 @@
|
|||||||
listWorkspaces: function() {
|
listWorkspaces: function() {
|
||||||
return get('/workspaces');
|
return get('/workspaces');
|
||||||
},
|
},
|
||||||
|
getCapabilities: function() {
|
||||||
|
return get('/capabilities');
|
||||||
|
},
|
||||||
createWorkspace: function(payload) {
|
createWorkspace: function(payload) {
|
||||||
return post('/workspaces', payload);
|
return post('/workspaces', payload);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -406,6 +406,23 @@ var TRANSLATIONS = {
|
|||||||
'settings.security.capabilities_title': 'More security options',
|
'settings.security.capabilities_title': 'More security options',
|
||||||
'settings.security.capabilities_body': 'Two-factor authentication, passkeys, and multi-session controls will be added later. The current live flow uses password sign-in with one HttpOnly browser session.',
|
'settings.security.capabilities_body': 'Two-factor authentication, passkeys, and multi-session controls will be added later. The current live flow uses password sign-in with one HttpOnly browser session.',
|
||||||
'settings.security.not_available': 'Available later.',
|
'settings.security.not_available': 'Available later.',
|
||||||
|
'settings.security.capability_title_community': 'Community build',
|
||||||
|
'settings.security.capability_title_enterprise': 'Enterprise build',
|
||||||
|
'settings.security.capability_title_cloud': 'Cloud build',
|
||||||
|
'settings.security.capability_summary': 'This build supports {protocols}, {access_modes}, and {security_levels} operation security. Limits: {max_workspaces} workspace, {max_users} user, {max_agents} AI agent. Short-lived tokens, one-time tokens, and advanced governance require a commercial edition.',
|
||||||
|
'settings.capability.rest': 'REST / HTTP',
|
||||||
|
'settings.capability.graphql': 'GraphQL',
|
||||||
|
'settings.capability.grpc': 'gRPC unary',
|
||||||
|
'settings.capability.websocket': 'WebSocket',
|
||||||
|
'settings.capability.soap': 'SOAP',
|
||||||
|
'settings.capability.standard': 'standard',
|
||||||
|
'settings.capability.elevated': 'elevated',
|
||||||
|
'settings.capability.strict': 'strict',
|
||||||
|
'settings.capability.static_agent_key': 'static agent keys',
|
||||||
|
'settings.capability.short_lived_token': 'short-lived tokens',
|
||||||
|
'settings.capability.one_time_token': 'one-time tokens',
|
||||||
|
'settings.capability.unlimited': 'unlimited',
|
||||||
|
'settings.capability.none': 'none',
|
||||||
'settings.session.title': 'Current session',
|
'settings.session.title': 'Current session',
|
||||||
'settings.session.browser': 'Browser session',
|
'settings.session.browser': 'Browser session',
|
||||||
'settings.session.current': 'current',
|
'settings.session.current': 'current',
|
||||||
@@ -1433,6 +1450,23 @@ var TRANSLATIONS = {
|
|||||||
'settings.security.capabilities_title': 'Дополнительные параметры безопасности',
|
'settings.security.capabilities_title': 'Дополнительные параметры безопасности',
|
||||||
'settings.security.capabilities_body': 'Двухфакторная аутентификация, passkeys и управление несколькими сессиями появятся позже. Сейчас доступен вход по паролю с одной HttpOnly browser session.',
|
'settings.security.capabilities_body': 'Двухфакторная аутентификация, passkeys и управление несколькими сессиями появятся позже. Сейчас доступен вход по паролю с одной HttpOnly browser session.',
|
||||||
'settings.security.not_available': 'Появится позже.',
|
'settings.security.not_available': 'Появится позже.',
|
||||||
|
'settings.security.capability_title_community': 'Редакция Community',
|
||||||
|
'settings.security.capability_title_enterprise': 'Редакция Enterprise',
|
||||||
|
'settings.security.capability_title_cloud': 'Редакция Cloud',
|
||||||
|
'settings.security.capability_summary': 'В этой сборке доступны {protocols}, {access_modes} и уровень защиты операций {security_levels}. Лимиты: {max_workspaces} воркспейс, {max_users} пользователь, {max_agents} AI-агент. Короткоживущие токены, одноразовые токены и расширенное управление доступом требуют коммерческую редакцию.',
|
||||||
|
'settings.capability.rest': 'REST / HTTP',
|
||||||
|
'settings.capability.graphql': 'GraphQL',
|
||||||
|
'settings.capability.grpc': 'gRPC unary',
|
||||||
|
'settings.capability.websocket': 'WebSocket',
|
||||||
|
'settings.capability.soap': 'SOAP',
|
||||||
|
'settings.capability.standard': 'standard',
|
||||||
|
'settings.capability.elevated': 'elevated',
|
||||||
|
'settings.capability.strict': 'strict',
|
||||||
|
'settings.capability.static_agent_key': 'статические ключи агентов',
|
||||||
|
'settings.capability.short_lived_token': 'короткоживущие токены',
|
||||||
|
'settings.capability.one_time_token': 'одноразовые токены',
|
||||||
|
'settings.capability.unlimited': 'без лимита',
|
||||||
|
'settings.capability.none': 'нет',
|
||||||
'settings.session.title': 'Текущая сессия',
|
'settings.session.title': 'Текущая сессия',
|
||||||
'settings.session.browser': 'Сессия браузера',
|
'settings.session.browser': 'Сессия браузера',
|
||||||
'settings.session.current': 'текущая',
|
'settings.session.current': 'текущая',
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
var settingsSession = null;
|
var settingsSession = null;
|
||||||
|
var settingsCapabilities = null;
|
||||||
function tKey(key) {
|
function tKey(key) {
|
||||||
return typeof t === 'function' ? t(key) : key;
|
return typeof t === 'function' ? t(key) : key;
|
||||||
}
|
}
|
||||||
@@ -70,6 +71,33 @@ function populateCurrentSession(session) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function capabilityLabel(key) {
|
||||||
|
return tKey('settings.capability.' + key);
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateCapabilities(capabilities) {
|
||||||
|
var title = document.getElementById('settings-security-capability-title');
|
||||||
|
var body = document.getElementById('settings-security-capability-body');
|
||||||
|
if (!title || !body || !capabilities) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var protocols = (capabilities.supported_protocols || []).map(capabilityLabel).join(', ');
|
||||||
|
var accessModes = (capabilities.machine_access_modes || []).map(capabilityLabel).join(', ');
|
||||||
|
var securityLevels = (capabilities.supported_security_levels || []).map(capabilityLabel).join(', ');
|
||||||
|
var limits = capabilities.limits || {};
|
||||||
|
|
||||||
|
title.textContent = tKey('settings.security.capability_title_' + capabilities.edition);
|
||||||
|
body.textContent = tfKey('settings.security.capability_summary', {
|
||||||
|
protocols: protocols || capabilityLabel('none'),
|
||||||
|
access_modes: accessModes || capabilityLabel('none'),
|
||||||
|
security_levels: securityLevels || capabilityLabel('none'),
|
||||||
|
max_workspaces: limits.max_workspaces == null ? capabilityLabel('unlimited') : String(limits.max_workspaces),
|
||||||
|
max_users: limits.max_users_per_workspace == null ? capabilityLabel('unlimited') : String(limits.max_users_per_workspace),
|
||||||
|
max_agents: limits.max_agents_per_workspace == null ? capabilityLabel('unlimited') : String(limits.max_agents_per_workspace),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function populateProfile(session) {
|
function populateProfile(session) {
|
||||||
if (!session || !session.user) {
|
if (!session || !session.user) {
|
||||||
return;
|
return;
|
||||||
@@ -101,6 +129,18 @@ async function loadProfile() {
|
|||||||
populateProfile(settingsSession);
|
populateProfile(settingsSession);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadCapabilities() {
|
||||||
|
if (!window.CrankApi || typeof window.CrankApi.getCapabilities !== 'function') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
settingsCapabilities = await window.CrankApi.getCapabilities();
|
||||||
|
populateCapabilities(settingsCapabilities);
|
||||||
|
} catch (_error) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function bindProfileSave() {
|
function bindProfileSave() {
|
||||||
var button = document.getElementById('settings-profile-save-btn');
|
var button = document.getElementById('settings-profile-save-btn');
|
||||||
if (!button || button.dataset.bound === 'true') {
|
if (!button || button.dataset.bound === 'true') {
|
||||||
@@ -330,6 +370,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
injectLanguageSwitcher();
|
injectLanguageSwitcher();
|
||||||
bindSectionNavigation();
|
bindSectionNavigation();
|
||||||
loadProfile();
|
loadProfile();
|
||||||
|
loadCapabilities();
|
||||||
bindProfileSave();
|
bindProfileSave();
|
||||||
bindPasswordSave();
|
bindPasswordSave();
|
||||||
loadWorkspaceSettings();
|
loadWorkspaceSettings();
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::Protocol;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum ProductEdition {
|
||||||
|
Community,
|
||||||
|
Enterprise,
|
||||||
|
Cloud,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum MachineAccessMode {
|
||||||
|
StaticAgentKey,
|
||||||
|
ShortLivedToken,
|
||||||
|
OneTimeToken,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum OperationSecurityLevel {
|
||||||
|
Standard,
|
||||||
|
Elevated,
|
||||||
|
Strict,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct EditionLimits {
|
||||||
|
pub max_workspaces: Option<u32>,
|
||||||
|
pub max_users_per_workspace: Option<u32>,
|
||||||
|
pub max_agents_per_workspace: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct EditionCapabilities {
|
||||||
|
pub edition: ProductEdition,
|
||||||
|
pub supported_protocols: Vec<Protocol>,
|
||||||
|
pub supported_security_levels: Vec<OperationSecurityLevel>,
|
||||||
|
pub machine_access_modes: Vec<MachineAccessMode>,
|
||||||
|
pub limits: EditionLimits,
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
pub mod access;
|
pub mod access;
|
||||||
pub mod agent;
|
pub mod agent;
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
|
pub mod edition;
|
||||||
pub mod ids;
|
pub mod ids;
|
||||||
pub mod observability;
|
pub mod observability;
|
||||||
pub mod operation;
|
pub mod operation;
|
||||||
@@ -20,6 +21,9 @@ pub use auth::{
|
|||||||
ApiKeyHeaderAuthConfig, ApiKeyQueryAuthConfig, AuthConfig, AuthProfile, BasicAuthConfig,
|
ApiKeyHeaderAuthConfig, ApiKeyQueryAuthConfig, AuthConfig, AuthProfile, BasicAuthConfig,
|
||||||
BearerAuthConfig,
|
BearerAuthConfig,
|
||||||
};
|
};
|
||||||
|
pub use edition::{
|
||||||
|
EditionCapabilities, EditionLimits, MachineAccessMode, OperationSecurityLevel, ProductEdition,
|
||||||
|
};
|
||||||
pub use ids::{
|
pub use ids::{
|
||||||
AgentId, AsyncJobId, AuthProfileId, DescriptorId, InvitationId, InvocationLogId, OperationId,
|
AgentId, AsyncJobId, AuthProfileId, DescriptorId, InvitationId, InvocationLogId, OperationId,
|
||||||
PlatformApiKeyId, SampleId, SecretId, StreamSessionId, ToolId, UserId, UserSessionId,
|
PlatformApiKeyId, SampleId, SecretId, StreamSessionId, ToolId, UserId, UserSessionId,
|
||||||
|
|||||||
@@ -50,6 +50,21 @@
|
|||||||
|
|
||||||
## 5. Группы endpoints
|
## 5. Группы endpoints
|
||||||
|
|
||||||
|
### 5.0. Build capabilities
|
||||||
|
|
||||||
|
- `GET /api/admin/capabilities`
|
||||||
|
|
||||||
|
Контракт:
|
||||||
|
|
||||||
|
- endpoint возвращает capability payload текущей сборки;
|
||||||
|
- payload описывает:
|
||||||
|
- `edition`
|
||||||
|
- `supported_protocols`
|
||||||
|
- `supported_security_levels`
|
||||||
|
- `machine_access_modes`
|
||||||
|
- `limits`
|
||||||
|
- UI использует этот ответ для edition-aware gating и честного product copy.
|
||||||
|
|
||||||
### 5.1. Workspaces and members
|
### 5.1. Workspaces and members
|
||||||
|
|
||||||
- `GET /api/admin/workspaces`
|
- `GET /api/admin/workspaces`
|
||||||
|
|||||||
Reference in New Issue
Block a user