api: expose community edition capabilities
This commit is contained in:
@@ -29,8 +29,9 @@ Implementation slices:
|
||||
4. Зафиксировать Community defaults в docs и runtime config.
|
||||
|
||||
Progress:
|
||||
- pending: define shared capability payload in `crank-core` and `admin-api`;
|
||||
- pending: expose Community capability defaults to UI;
|
||||
- done: shared capability payload exists in `crank-core` and `admin-api`;
|
||||
- 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.
|
||||
|
||||
DoD:
|
||||
|
||||
@@ -23,6 +23,7 @@ use crate::{
|
||||
update_profile,
|
||||
},
|
||||
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},
|
||||
operations::{
|
||||
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));
|
||||
|
||||
let workspace_root_router = Router::new()
|
||||
.route("/capabilities", get(get_capabilities))
|
||||
.route("/workspaces", get(list_workspaces).post(create_workspace))
|
||||
.layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
@@ -999,6 +1001,43 @@ mod tests {
|
||||
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")]
|
||||
#[serial]
|
||||
async fn manages_workspace_access_lifecycle() {
|
||||
|
||||
@@ -2,6 +2,7 @@ pub mod access;
|
||||
pub mod agents;
|
||||
pub mod auth;
|
||||
pub mod auth_profiles;
|
||||
pub mod capabilities;
|
||||
pub mod observability;
|
||||
pub mod operations;
|
||||
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_core::{
|
||||
Agent, AgentId, AgentOperationBinding, AgentStatus, AgentVersion, AggregationMode,
|
||||
AsyncJobHandle, AuthConfig, AuthKind, AuthProfile, AuthProfileId, ConfigExport, ExecutionMode,
|
||||
ExportMode, GeneratedDraft, GeneratedDraftStatus, InvitationId, InvitationStatus,
|
||||
InvitationToken, InvocationLevel, InvocationLog, InvocationLogId, InvocationSource,
|
||||
InvocationStatus, JobStatus, MembershipRole, OperationId, OperationStatus, PlatformApiKey,
|
||||
PlatformApiKeyId, PlatformApiKeyScope, PlatformApiKeyStatus, Protocol, SampleId, Samples,
|
||||
Secret, SecretId, SecretKind, SecretStatus, StreamSession, StreamStatus, Target,
|
||||
TransportBehavior, UsagePeriod, UserId, UserSessionId, Workspace, WorkspaceId, WorkspaceStatus,
|
||||
AsyncJobHandle, AuthConfig, AuthKind, AuthProfile, AuthProfileId, ConfigExport,
|
||||
EditionCapabilities, EditionLimits, ExecutionMode, ExportMode, GeneratedDraft,
|
||||
GeneratedDraftStatus, InvitationId, InvitationStatus, InvitationToken, InvocationLevel,
|
||||
InvocationLog, InvocationLogId, InvocationSource, InvocationStatus, JobStatus,
|
||||
MachineAccessMode, MembershipRole, OperationId, OperationSecurityLevel, OperationStatus,
|
||||
PlatformApiKey, PlatformApiKeyId, PlatformApiKeyScope, PlatformApiKeyStatus, ProductEdition,
|
||||
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_proto::{ProtoService, services_from_descriptor_set_bytes};
|
||||
@@ -1566,6 +1568,20 @@ impl AdminService {
|
||||
.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))]
|
||||
pub async fn list_stream_sessions(
|
||||
&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"/>
|
||||
</svg>
|
||||
<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>
|
||||
|
||||
@@ -172,6 +172,9 @@
|
||||
listWorkspaces: function() {
|
||||
return get('/workspaces');
|
||||
},
|
||||
getCapabilities: function() {
|
||||
return get('/capabilities');
|
||||
},
|
||||
createWorkspace: function(payload) {
|
||||
return post('/workspaces', payload);
|
||||
},
|
||||
|
||||
@@ -406,6 +406,23 @@ var TRANSLATIONS = {
|
||||
'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.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.browser': 'Browser session',
|
||||
'settings.session.current': 'current',
|
||||
@@ -1433,6 +1450,23 @@ var TRANSLATIONS = {
|
||||
'settings.security.capabilities_title': 'Дополнительные параметры безопасности',
|
||||
'settings.security.capabilities_body': 'Двухфакторная аутентификация, passkeys и управление несколькими сессиями появятся позже. Сейчас доступен вход по паролю с одной HttpOnly browser session.',
|
||||
'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.browser': 'Сессия браузера',
|
||||
'settings.session.current': 'текущая',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
var settingsSession = null;
|
||||
var settingsCapabilities = null;
|
||||
function tKey(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) {
|
||||
if (!session || !session.user) {
|
||||
return;
|
||||
@@ -101,6 +129,18 @@ async function loadProfile() {
|
||||
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() {
|
||||
var button = document.getElementById('settings-profile-save-btn');
|
||||
if (!button || button.dataset.bound === 'true') {
|
||||
@@ -330,6 +370,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
injectLanguageSwitcher();
|
||||
bindSectionNavigation();
|
||||
loadProfile();
|
||||
loadCapabilities();
|
||||
bindProfileSave();
|
||||
bindPasswordSave();
|
||||
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 agent;
|
||||
pub mod auth;
|
||||
pub mod edition;
|
||||
pub mod ids;
|
||||
pub mod observability;
|
||||
pub mod operation;
|
||||
@@ -20,6 +21,9 @@ pub use auth::{
|
||||
ApiKeyHeaderAuthConfig, ApiKeyQueryAuthConfig, AuthConfig, AuthProfile, BasicAuthConfig,
|
||||
BearerAuthConfig,
|
||||
};
|
||||
pub use edition::{
|
||||
EditionCapabilities, EditionLimits, MachineAccessMode, OperationSecurityLevel, ProductEdition,
|
||||
};
|
||||
pub use ids::{
|
||||
AgentId, AsyncJobId, AuthProfileId, DescriptorId, InvitationId, InvocationLogId, OperationId,
|
||||
PlatformApiKeyId, SampleId, SecretId, StreamSessionId, ToolId, UserId, UserSessionId,
|
||||
|
||||
@@ -50,6 +50,21 @@
|
||||
|
||||
## 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
|
||||
|
||||
- `GET /api/admin/workspaces`
|
||||
|
||||
Reference in New Issue
Block a user