Remove enterprise leftovers from community
CI / Rust Checks (push) Failing after 2m6s
CI / UI Checks (push) Has been skipped
CI / Frontend E2E (push) Has been skipped
CI / Deployment Manifests (push) Has been skipped
Deploy / deploy (push) Successful in 2m26s

This commit is contained in:
github-ops
2026-06-20 12:04:46 +00:00
parent 5f8c208409
commit 0af60b1693
46 changed files with 46 additions and 7096 deletions
+2 -2
View File
@@ -36,7 +36,7 @@
- распространять вклад;
- публиковать вклад;
- сублицензировать вклад;
- включать вклад в открытые, коммерческие и закрытые версии Crank.
- включать вклад в проект Crank и производные работы.
## 4. Патенты
@@ -50,7 +50,7 @@
Crank Community распространяется по лицензии GNU Affero General Public License v3.0 only.
Владелец проекта может использовать принятые вклады также в других редакциях Crank, включая коммерческие, облачные и закрытые редакции.
Владелец проекта может использовать принятые вклады в проекте Crank и производных работах.
## 7. Как подтвердить согласие
+3 -3
View File
@@ -6,7 +6,7 @@ Crank Community принимает исправления ошибок, улуч
Crank Community распространяется по лицензии GNU Affero General Public License v3.0 only.
Перед отправкой pull request нужно согласиться с [Contributor License Agreement](./CLA.md). Это нужно, чтобы проект мог использовать принятые изменения не только в Community-версии, но и в коммерческих редакциях Crank.
Перед отправкой pull request нужно согласиться с [Contributor License Agreement](./CLA.md). Это нужно, чтобы права на принятые изменения были оформлены явно и проект мог развиваться без юридических неопределенностей.
## Перед pull request
@@ -35,7 +35,7 @@ npx playwright test
- Для изменений SQL-запросов обновляйте `.sqlx`, если это требуется SQLx.
- Для пользовательских изменений обновляйте документацию или примеры.
## Границы Community-версии
## Границы проекта
В этом репозитории поддерживаются:
@@ -46,4 +46,4 @@ npx playwright test
- PostgreSQL как основное хранилище;
- необязательный Valkey или Redis для служебного кэша.
Функции коммерческих редакций не должны попадать в этот репозиторий.
Функции за пределами перечисленного набора не должны попадать в этот репозиторий.
+1 -1
View File
@@ -4,4 +4,4 @@ Copyright 2026 bsodfather
This product includes software developed for the Crank project.
Crank Community is licensed under the GNU Affero General Public License
version 3 only. Commercial licensing for other editions is handled separately.
version 3 only.
+1 -1450
View File
File diff suppressed because it is too large Load Diff
+1 -62
View File
@@ -109,13 +109,6 @@ impl ApiError {
}
}
pub(crate) fn forbidden_with_context(message: impl Into<String>, context: Value) -> Self {
Self::Forbidden {
message: message.into(),
context: Some(context),
}
}
fn status_code(&self) -> StatusCode {
match self {
Self::Unauthorized { .. } => StatusCode::UNAUTHORIZED,
@@ -224,14 +217,6 @@ impl From<RegistryError> for ApiError {
format!("secret {secret_id} was not found"),
json!({ "secret_id": secret_id }),
),
RegistryError::StreamSessionNotFound { session_id } => Self::not_found_with_context(
format!("stream session {session_id} was not found"),
json!({ "session_id": session_id }),
),
RegistryError::AsyncJobNotFound { job_id } => Self::not_found_with_context(
format!("async job {job_id} was not found"),
json!({ "job_id": job_id }),
),
RegistryError::InvocationLogNotFound { log_id } => Self::not_found_with_context(
format!("invocation log {log_id} was not found"),
json!({ "log_id": log_id }),
@@ -299,28 +284,6 @@ impl From<RegistryError> for ApiError {
"auth_profile_id": auth_profile_id,
}),
),
RegistryError::InvalidStreamSessionTransition {
session_id,
from,
to,
} => Self::conflict_with_context(
format!("invalid stream session transition for {session_id}: {from} -> {to}"),
json!({
"session_id": session_id,
"from": from,
"to": to,
}),
),
RegistryError::InvalidAsyncJobTransition { job_id, from, to } => {
Self::conflict_with_context(
format!("invalid async job transition for {job_id}: {from} -> {to}"),
json!({
"job_id": job_id,
"from": from,
"to": to,
}),
)
}
RegistryError::UserEmailAlreadyExists { email } => Self::conflict_with_context(
format!("user with email {email} already exists"),
json!({ "email": email }),
@@ -490,8 +453,7 @@ pub fn runtime_error_context(error: &RuntimeError) -> Option<Value> {
mod tests {
use serde_json::json;
use super::{ApiError, runtime_error_context, runtime_test_failure};
use crank_registry::RegistryError;
use super::{runtime_error_context, runtime_test_failure};
use crank_runtime::RuntimeError;
#[test]
@@ -544,27 +506,4 @@ mod tests {
})
);
}
#[test]
fn registry_errors_preserve_structured_context_in_api_error() {
let error = ApiError::from(RegistryError::InvalidAsyncJobTransition {
job_id: "job_123".to_owned(),
from: "running".to_owned(),
to: "completed".to_owned(),
});
match error {
ApiError::Conflict { context, .. } => {
assert_eq!(
context,
Some(json!({
"job_id": "job_123",
"from": "running",
"to": "completed"
}))
);
}
other => panic!("unexpected error variant: {other:?}"),
}
}
}
-2
View File
@@ -3,11 +3,9 @@ pub mod agents;
pub mod auth;
pub mod auth_profiles;
pub mod capabilities;
pub mod machine_auth;
pub mod observability;
pub mod operations;
pub mod secrets;
pub mod streaming;
pub mod upstreams;
pub mod workspaces;
-124
View File
@@ -1,124 +0,0 @@
use axum::{Json, extract::State};
use crank_core::{
IssueAgentTokenRequest, IssueOneTimeAgentTokenRequest, MachineAccessMode, MembershipRole,
TokenIssuerActor, TokenIssuerError, UserId, WorkspaceId,
};
use serde_json::json;
use crate::{error::ApiError, state::AppState};
pub async fn issue_agent_token(
State(state): State<AppState>,
Json(payload): Json<IssueAgentTokenRequest>,
) -> Result<Json<serde_json::Value>, ApiError> {
let edition = state.service.capability_profile().capabilities().edition;
let grant_type = payload.grant_type.clone();
let response = state
.service
.token_issuer()
.issue_short_lived(payload, &community_token_issuer_actor())
.await
.map_err(|error| {
map_token_issuer_error(
error,
edition,
MachineAccessMode::ShortLivedToken,
json!({
"grant_type": grant_type,
"upgrade_required": true,
}),
)
})?;
Ok(Json(serde_json::json!(response)))
}
pub async fn issue_one_time_agent_token(
State(state): State<AppState>,
Json(payload): Json<IssueOneTimeAgentTokenRequest>,
) -> Result<Json<serde_json::Value>, ApiError> {
let edition = state.service.capability_profile().capabilities().edition;
let operation_id = payload.operation_id.as_str().to_owned();
let response = state
.service
.token_issuer()
.issue_one_time(payload, &community_token_issuer_actor())
.await
.map_err(|error| {
map_token_issuer_error(
error,
edition,
MachineAccessMode::OneTimeToken,
json!({
"operation_id": operation_id,
"upgrade_required": true,
}),
)
})?;
Ok(Json(serde_json::json!(response)))
}
fn community_token_issuer_actor() -> TokenIssuerActor {
TokenIssuerActor {
user_id: UserId::new("user_community_public"),
workspace_id: WorkspaceId::new("ws_community_public"),
role: MembershipRole::Viewer,
}
}
fn map_token_issuer_error(
error: TokenIssuerError,
edition: crank_core::ProductEdition,
machine_access_mode: MachineAccessMode,
extra_context: serde_json::Value,
) -> ApiError {
match error {
TokenIssuerError::NotSupportedInEdition => ApiError::forbidden_with_context(
match machine_access_mode {
MachineAccessMode::ShortLivedToken => {
"short-lived machine access is not available in Community"
}
MachineAccessMode::OneTimeToken => {
"one-time machine access is not available in Community"
}
MachineAccessMode::StaticAgentKey => {
"static agent key machine access is not available for token issue"
}
},
merge_machine_auth_context(edition, machine_access_mode, extra_context),
),
TokenIssuerError::InvalidGrant(reason) => ApiError::validation_with_context(
"invalid machine token grant",
json!({ "reason": reason }),
),
TokenIssuerError::AgentKeyUnknown => ApiError::validation("agent key is unknown"),
TokenIssuerError::OperationNotStrict => ApiError::validation("operation is not strict"),
TokenIssuerError::OperationNotPublishedForAgent => {
ApiError::validation("operation is not published for agent")
}
TokenIssuerError::RegistryFailure(details) => {
ApiError::internal(format!("token issuer registry failure: {details}"))
}
TokenIssuerError::ReplayGuardFailure(details) => {
ApiError::internal(format!("token issuer replay guard failure: {details}"))
}
}
}
fn merge_machine_auth_context(
edition: crank_core::ProductEdition,
machine_access_mode: MachineAccessMode,
extra_context: serde_json::Value,
) -> serde_json::Value {
let mut context = json!({
"edition": edition,
"machine_access_mode": machine_access_mode,
});
if let (Some(base), Some(extra)) = (context.as_object_mut(), extra_context.as_object()) {
for (key, value) in extra {
base.insert(key.clone(), value.clone());
}
}
context
}
-16
View File
@@ -1,16 +0,0 @@
use axum::{
Json,
extract::{Path, State},
};
use serde_json::{Value, json};
use crate::{error::ApiError, routes::access::WorkspacePath, state::AppState};
pub async fn list_protocol_capabilities(
Path(_path): Path<WorkspacePath>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
Ok(Json(json!({
"items": state.service.list_protocol_capabilities().await
})))
}
+8 -242
View File
@@ -8,12 +8,12 @@ use crank_core::{
AuthKind, AuthProfile, AuthProfileId, CapabilityProfile, CommunityCapabilityProfile,
ConfigExport, EditionCapabilities, ExecutionMode, ExportMode, GeneratedDraft,
GeneratedDraftStatus, IdentityError, IdentityProvider, InvocationLevel, InvocationLog,
InvocationLogId, InvocationSource, InvocationStatus, LoginOutcome, MachineTokenIssuer,
MembershipRole, NoMachineTokenIssuer, NoopAuditSink, OperationId, OperationSecurityLevel,
OperationStatus, OwnerOnlyPolicyEngine, PlatformApiKey, PlatformApiKeyId, PlatformApiKeyScope,
PlatformApiKeyStatus, PolicyEngine, ProductEdition, Protocol, ResponseCachePolicy, SampleId,
Samples, Secret, SecretId, SecretKind, SecretStatus, Target, UsagePeriod, UserId,
UserSessionId, WizardState, Workspace, WorkspaceId, WorkspaceStatus,
InvocationLogId, InvocationSource, InvocationStatus, LoginOutcome, MembershipRole,
NoopAuditSink, OperationId, OperationSecurityLevel, OperationStatus, OwnerOnlyPolicyEngine,
PlatformApiKey, PlatformApiKeyId, PlatformApiKeyScope, PlatformApiKeyStatus, PolicyEngine,
ProductEdition, Protocol, ResponseCachePolicy, SampleId, Samples, Secret, SecretId, SecretKind,
SecretStatus, Target, UsagePeriod, UserId, UserSessionId, WizardState, Workspace, WorkspaceId,
WorkspaceStatus,
};
use crank_mapping::{JsonPathRoot, MappingSet, infer_mapping_from_samples};
use crank_registry::{
@@ -59,7 +59,6 @@ pub struct AdminService {
identity_provider: Option<Arc<dyn IdentityProvider>>,
policy_engine: Arc<dyn PolicyEngine>,
audit_sink: Arc<dyn AuditSink>,
token_issuer: Arc<dyn MachineTokenIssuer>,
capability_profile: Arc<dyn CapabilityProfile>,
}
@@ -72,7 +71,6 @@ pub struct AdminServiceBuilder {
identity_provider: Option<Arc<dyn IdentityProvider>>,
policy_engine: Option<Arc<dyn PolicyEngine>>,
audit_sink: Option<Arc<dyn AuditSink>>,
token_issuer: Option<Arc<dyn MachineTokenIssuer>>,
capability_profile: Option<Arc<dyn CapabilityProfile>>,
}
@@ -508,21 +506,6 @@ pub struct DescriptorUploadResponse {
pub version: u32,
}
#[derive(Clone, Debug, Serialize)]
pub struct GrpcServiceSummary {
pub package: String,
pub service: String,
pub methods: Vec<GrpcMethodSummary>,
}
#[derive(Clone, Debug, Serialize)]
pub struct GrpcMethodSummary {
pub name: String,
pub kind: String,
pub input_schema: Schema,
pub output_schema: Schema,
}
fn default_operation_category() -> String {
"general".to_owned()
}
@@ -574,10 +557,6 @@ impl AdminService {
&self.audit_sink
}
pub fn token_issuer(&self) -> &Arc<dyn MachineTokenIssuer> {
&self.token_issuer
}
pub fn capability_profile(&self) -> &Arc<dyn CapabilityProfile> {
&self.capability_profile
}
@@ -600,7 +579,6 @@ impl AdminServiceBuilder {
identity_provider: None,
policy_engine: None,
audit_sink: None,
token_issuer: None,
capability_profile: None,
}
}
@@ -622,12 +600,6 @@ impl AdminServiceBuilder {
self
}
#[allow(dead_code)]
pub fn with_token_issuer(mut self, token_issuer: Arc<dyn MachineTokenIssuer>) -> Self {
self.token_issuer = Some(token_issuer);
self
}
#[allow(dead_code)]
pub fn with_capability_profile(
mut self,
@@ -649,9 +621,6 @@ impl AdminServiceBuilder {
.policy_engine
.unwrap_or_else(|| Arc::new(OwnerOnlyPolicyEngine)),
audit_sink: self.audit_sink.unwrap_or_else(|| Arc::new(NoopAuditSink)),
token_issuer: self
.token_issuer
.unwrap_or_else(|| Arc::new(NoMachineTokenIssuer)),
capability_profile: self
.capability_profile
.unwrap_or_else(|| Arc::new(CommunityCapabilityProfile)),
@@ -789,9 +758,6 @@ impl AdminService {
.await
{
Ok(LoginOutcome::Authenticated(identity)) => Ok(identity),
Ok(LoginOutcome::TwoFactorRequired(_)) => Err(ApiError::internal(
"two-factor login flow is not configured for this service",
)),
Err(error) => Err(map_identity_error(error)),
};
}
@@ -1818,204 +1784,6 @@ impl AdminService {
.map(Some)
}
#[cfg(any())]
async fn start_stream_session_test(
&self,
workspace_id: &WorkspaceId,
operation: &RegistryOperation,
runtime: &RuntimeOperation,
input: &Value,
resolved_auth: Option<&ResolvedAuth>,
request_context: &RuntimeRequestContext,
) -> Result<StreamSessionStartView, RuntimeError> {
let streaming = runtime.execution_config.streaming.as_ref().ok_or_else(|| {
RuntimeError::MissingStreamingConfig {
operation_id: runtime.operation_id.as_str().to_owned(),
}
})?;
let seed = self
.runtime
.execute_session_seed_with_auth_and_context(
runtime,
input,
resolved_auth,
Some(request_context),
)
.await?;
let batch_size = streaming.max_items.unwrap_or(10).max(1) as usize;
let preview_count = seed.items.len().min(batch_size);
let preview_items = seed.items[..preview_count].to_vec();
let next_index = preview_count;
let created_at = OffsetDateTime::now_utc();
let expires_at = created_at
.checked_add(time::Duration::milliseconds(
streaming.max_session_lifetime_ms.unwrap_or(60_000) as i64,
))
.ok_or_else(|| RuntimeError::SecretCrypto {
operation: "compute stream session expiration",
details: "failed to compute stream session expiration".to_owned(),
})?;
let session = StreamSession {
id: crank_core::StreamSessionId::new(new_prefixed_id("sess")),
workspace_id: workspace_id.clone(),
agent_id: None,
operation_id: operation.id.clone(),
protocol: operation.protocol,
mode: ExecutionMode::Session,
status: StreamStatus::Running,
cursor: (next_index < seed.items.len()).then(|| json!(next_index)),
state: json!(StoredSessionState {
input: input.clone(),
summary: seed.summary.clone(),
items: seed.items.clone(),
next_index,
batch_size,
}),
expires_at,
last_poll_at: None,
created_at,
closed_at: None,
};
self.registry
.create_stream_session(CreateStreamSessionRequest { session: &session })
.await
.map_err(|error| RuntimeError::SecretCrypto {
operation: "persist stream session",
details: error.to_string(),
})?;
Ok(StreamSessionStartView {
session_id: session.id.as_str().to_owned(),
status: session.status,
expires_at,
poll_after_ms: streaming.poll_interval_ms.unwrap_or(1_000),
preview: json!({
"summary": seed.summary,
"items": preview_items,
}),
})
}
#[cfg(any())]
async fn start_async_job_test(
&self,
workspace_id: &WorkspaceId,
operation: &RegistryOperation,
runtime: &RuntimeOperation,
input: &Value,
request_context: &RuntimeRequestContext,
) -> Result<AsyncJobStartView, RuntimeError> {
let created_at = OffsetDateTime::now_utc();
let streaming = runtime.execution_config.streaming.as_ref().ok_or_else(|| {
RuntimeError::MissingStreamingConfig {
operation_id: runtime.operation_id.as_str().to_owned(),
}
})?;
let expires_at = created_at
.checked_add(time::Duration::milliseconds(
streaming.max_session_lifetime_ms.unwrap_or(300_000) as i64,
))
.ok_or_else(|| RuntimeError::SecretCrypto {
operation: "compute async job expiration",
details: "failed to compute async job expiration".to_owned(),
})?;
let job = AsyncJobHandle {
id: crank_core::AsyncJobId::new(new_prefixed_id("job")),
workspace_id: workspace_id.clone(),
agent_id: None,
operation_id: operation.id.clone(),
status: JobStatus::Running,
progress: json!({ "pct": 0 }),
result: None,
error: None,
expires_at: Some(expires_at),
last_poll_at: None,
created_at,
updated_at: created_at,
finished_at: None,
};
self.registry
.create_async_job(CreateAsyncJobRequest { job: &job })
.await
.map_err(|error| RuntimeError::SecretCrypto {
operation: "persist async job",
details: error.to_string(),
})?;
let registry = self.registry.clone();
let secret_crypto = self.secret_crypto.clone();
let runtime_for_task = self.runtime.clone();
let workspace_for_task = workspace_id.clone();
let operation_for_task = runtime.clone();
let input_for_task = input.clone();
let request_context_for_task = request_context.clone();
let job_id = job.id.clone();
tokio::spawn(async move {
let resolved_auth = resolve_runtime_auth_for_task(
&registry,
&secret_crypto,
&workspace_for_task,
&operation_for_task.execution_config,
)
.await;
let result = match resolved_auth {
Ok(resolved_auth) => {
runtime_for_task
.execute_with_auth_and_context(
&operation_for_task,
&input_for_task,
resolved_auth.as_ref(),
Some(&request_context_for_task),
)
.await
}
Err(error) => Err(error),
};
let finished_at = OffsetDateTime::now_utc();
let _ = match result {
Ok(output) => {
registry
.update_async_job_status(UpdateAsyncJobStatusRequest {
job_id: &job_id,
current_status: JobStatus::Running,
next_status: JobStatus::Completed,
progress: &json!({ "pct": 100 }),
result: Some(&output),
error: None,
expires_at: None,
updated_at: &finished_at,
finished_at: Some(&finished_at),
})
.await
}
Err(error) => {
registry
.update_async_job_status(UpdateAsyncJobStatusRequest {
job_id: &job_id,
current_status: JobStatus::Running,
next_status: JobStatus::Failed,
progress: &json!({ "pct": 100 }),
result: None,
error: Some(&json!({
"code": runtime_error_code(&error),
"message": error.to_string(),
})),
expires_at: None,
updated_at: &finished_at,
finished_at: Some(&finished_at),
})
.await
}
};
});
Ok(AsyncJobStartView {
job_id: job.id.as_str().to_owned(),
status: job.status,
progress: job.progress,
})
}
async fn resolve_auth_profile(
&self,
workspace_id: &WorkspaceId,
@@ -3419,8 +3187,6 @@ impl AdminService {
"path": {},
"query": {},
"headers": { "x-demo-source": "crank-seed" },
"variables": null,
"grpc": null,
"body": demo_rest_request_sample()
}),
response_preview: demo_rest_response_sample(),
@@ -3602,9 +3368,9 @@ fn demo_revops_agent_payload() -> AgentPayload {
AgentPayload {
slug: "revops-copilot".to_owned(),
display_name: "RevOps Copilot".to_owned(),
description: "Revenue operations assistant with CRM and billing tools.".to_owned(),
description: "Sales operations assistant with CRM tools.".to_owned(),
instructions: json!({
"system": "Prefer CRM mutations first, then billing lookups for confirmation."
"system": "Prefer CRM mutations first, then lookup tools for confirmation."
}),
tool_selection_policy: json!({
"max_tools": 8,
File diff suppressed because it is too large Load Diff
-3
View File
@@ -334,9 +334,6 @@
letter-spacing: 0.02em;
}
.badge-rest { color: var(--blue); background: var(--blue-bg); border-color: var(--blue-border); }
.badge-graphql { color: var(--purple); background: var(--purple-bg); border-color: var(--purple-border); }
.badge-grpc { color: #14b8a6; background: rgba(13,148,136,0.1); border-color: rgba(13,148,136,0.25); }
.status-badge {
display: inline-flex;
align-items: center;
-32
View File
@@ -197,38 +197,6 @@
.login-divider-line { flex: 1; height: 1px; background: var(--border); }
.login-divider-text { font-size: 11.5px; color: var(--text-muted); white-space: nowrap; }
.btn-sso {
width: 100%;
padding: 9px;
background: var(--bg-overlay);
color: var(--text-secondary);
border: 1px solid var(--border);
border-radius: 7px;
font-family: 'Inter', sans-serif;
font-size: 13.5px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s;
display: flex;
align-items: center;
justify-content: center;
gap: 9px;
}
.btn-sso:hover { background: var(--bg-muted); color: var(--text-primary); }
.btn-sso:disabled {
cursor: not-allowed;
opacity: 0.7;
background: var(--bg-overlay);
color: var(--text-muted);
}
.btn-sso:disabled:hover {
background: var(--bg-overlay);
color: var(--text-muted);
}
.login-inline-badge {
display: inline-flex;
align-items: center;
-437
View File
@@ -534,31 +534,11 @@
border: 1px solid rgba(88, 166, 255, 0.2);
}
.protocol-card.graphql .protocol-icon-wrap {
background: rgba(188, 140, 255, 0.12);
border: 1px solid rgba(188, 140, 255, 0.2);
}
.protocol-card.grpc .protocol-icon-wrap {
background: rgba(13, 148, 136, 0.12);
border: 1px solid rgba(13, 148, 136, 0.2);
}
.protocol-card.selected.rest .protocol-icon-wrap {
background: rgba(88, 166, 255, 0.18);
border-color: rgba(88, 166, 255, 0.35);
}
.protocol-card.selected.graphql .protocol-icon-wrap {
background: rgba(188, 140, 255, 0.18);
border-color: rgba(188, 140, 255, 0.35);
}
.protocol-card.selected.grpc .protocol-icon-wrap {
background: rgba(13, 148, 136, 0.2);
border-color: rgba(13, 148, 136, 0.4);
}
.protocol-card-name {
font-size: 15px;
font-weight: 700;
@@ -608,12 +588,6 @@
color: #58a6ff;
}
.protocol-card.graphql.selected .protocol-tag {
background: rgba(188, 140, 255, 0.1);
border-color: rgba(188, 140, 255, 0.25);
color: #bc8cff;
}
/* ── REST method picker ── */
.method-section {
background: var(--bg-surface);
@@ -1601,414 +1575,3 @@
.method-callout[hidden] {
display: none !important;
}
/*
GraphQL operation type cards (Step 4 GraphQL)
*/
.gql-type-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.gql-type-card {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 6px;
padding: 16px 18px;
background: var(--bg-canvas);
border: 1.5px solid var(--border);
border-radius: 8px;
cursor: pointer;
transition: border-color 0.15s, background 0.15s, box-shadow 0.15s;
text-align: left;
font-family: 'Inter', sans-serif;
}
.gql-type-card:hover {
border-color: var(--accent);
background: rgba(13, 148, 136, 0.04);
}
.gql-type-card.active {
border-color: var(--accent);
background: var(--accent-glow);
box-shadow: 0 0 0 3px rgba(13, 148, 136, 0.15);
}
.gql-type-keyword {
font-size: 15px;
font-weight: 700;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
color: var(--accent);
letter-spacing: -0.3px;
}
.gql-type-card.active .gql-type-keyword { color: #5eead4; }
.gql-type-desc {
font-size: 12px;
color: var(--text-muted);
line-height: 1.4;
}
.gql-type-card.active .gql-type-desc { color: var(--text-secondary); }
/*
Protocol card disabled tag (Streaming)
*/
.protocol-tag-disabled {
text-decoration: line-through;
color: var(--error, #f87171);
opacity: 0.75;
}
/*
gRPC Proto upload
*/
.proto-dropzone {
border: 1.5px dashed var(--border);
border-radius: 10px;
padding: 36px 24px;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
cursor: pointer;
transition: border-color 0.15s, background 0.15s;
background: var(--bg-canvas);
}
.proto-dropzone:hover,
.proto-dropzone.drag-over {
border-color: var(--accent);
background: rgba(56,139,253,0.05);
}
.proto-dropzone.drag-over { border-style: solid; }
.proto-dropzone-title {
font-size: 13px;
font-weight: 500;
color: var(--text-primary);
}
.proto-dropzone-sub {
font-size: 12px;
color: var(--text-muted);
}
.proto-file-info {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
background: rgba(56,139,253,0.07);
border: 1px solid rgba(56,139,253,0.25);
border-radius: 8px;
}
.proto-file-name {
font-family: var(--font-mono, monospace);
font-size: 13px;
color: var(--accent);
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.proto-file-size {
font-size: 11px;
color: var(--text-muted);
white-space: nowrap;
}
.proto-file-change {
background: none;
border: 1px solid var(--border);
border-radius: 5px;
color: var(--text-secondary);
font-size: 11px;
padding: 3px 9px;
cursor: pointer;
white-space: nowrap;
transition: border-color 0.15s, color 0.15s;
}
.proto-file-change:hover { border-color: var(--accent); color: var(--accent); }
/*
gRPC Service/method list
*/
.proto-service-group {
padding-top: 16px;
}
.proto-service-group + .proto-service-group {
border-top: 1px solid var(--border-subtle);
margin-top: 4px;
}
.proto-service-label {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-muted);
margin-bottom: 10px;
}
.proto-service-label svg { opacity: 0.5; }
.proto-method-grid {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.proto-method-card {
display: flex;
flex-direction: column;
gap: 4px;
padding: 10px 14px;
background: var(--bg-canvas);
border: 1.5px solid var(--border);
border-radius: 8px;
cursor: pointer;
text-align: left;
transition: border-color 0.15s, background 0.15s;
min-width: 160px;
}
.proto-method-card:hover {
border-color: var(--border-muted, var(--accent));
background: rgba(56,139,253,0.04);
}
.proto-method-card.active {
border-color: var(--accent);
background: rgba(56,139,253,0.08);
}
.proto-method-name {
font-family: var(--font-mono, monospace);
font-size: 13px;
font-weight: 600;
color: var(--text-primary);
}
.proto-method-card.active .proto-method-name { color: var(--accent); }
.proto-method-types {
font-size: 11px;
color: var(--text-muted);
display: flex;
align-items: center;
gap: 4px;
}
.proto-method-types svg { opacity: 0.5; flex-shrink: 0; }
.proto-empty {
padding: 24px;
text-align: center;
color: var(--text-muted);
font-size: 13px;
}
/*
gRPC Method signature detail
*/
.proto-path-row {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 12px;
background: var(--bg-canvas);
border: 1px solid var(--border-subtle);
border-radius: 6px;
}
.proto-path-label {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-muted);
white-space: nowrap;
}
.proto-path-value {
font-family: var(--font-mono, monospace);
font-size: 12px;
color: var(--accent);
word-break: break-all;
}
.proto-sig-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
@media (max-width: 680px) {
.proto-sig-grid { grid-template-columns: 1fr; }
}
.proto-sig-col {
display: flex;
flex-direction: column;
gap: 0;
border: 1px solid var(--border-subtle);
border-radius: 8px;
overflow: hidden;
}
.proto-sig-col-header {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
background: var(--bg-surface);
border-bottom: 1px solid var(--border-subtle);
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-secondary);
}
.proto-sig-col-header svg { opacity: 0.6; }
.proto-type-badge {
font-family: var(--font-mono, monospace);
font-size: 11px;
background: rgba(56,139,253,0.12);
color: var(--accent);
padding: 1px 6px;
border-radius: 4px;
margin-left: auto;
}
.proto-field-list {
padding: 6px 0;
}
.proto-field-row {
display: flex;
align-items: baseline;
justify-content: space-between;
padding: 5px 12px;
font-size: 12px;
border-bottom: 1px solid var(--border-subtle);
}
.proto-field-row:last-child { border-bottom: none; }
.proto-field-name {
font-family: var(--font-mono, monospace);
color: var(--text-primary);
font-size: 12px;
}
.proto-field-type {
font-family: var(--font-mono, monospace);
font-size: 11px;
color: var(--text-muted);
}
.proto-field-empty {
padding: 16px 12px;
font-size: 12px;
color: var(--text-secondary);
text-align: center;
line-height: 1.5;
}
/*
gRPC Source tabs
*/
.grpc-source-tabs {
display: flex;
gap: 0;
margin-bottom: 20px;
background: var(--bg-canvas);
border: 1px solid var(--border);
border-radius: 9px;
padding: 3px;
}
.grpc-source-btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 7px;
padding: 8px 14px;
background: none;
border: none;
border-radius: 7px;
font-size: 13px;
font-weight: 500;
color: var(--text-muted);
cursor: pointer;
transition: background 0.15s, color 0.15s;
white-space: nowrap;
}
.grpc-source-btn svg { flex-shrink: 0; opacity: 0.7; }
.grpc-source-btn:hover { color: var(--text-secondary); background: rgba(255,255,255,0.04); }
.grpc-source-btn.active {
background: var(--bg-surface);
color: var(--text-primary);
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
}
.grpc-source-btn.active svg { opacity: 1; }
/*
gRPC Reflection panel
*/
.grpc-reflect-upstream {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
background: var(--bg-canvas);
border: 1px solid var(--border-subtle);
border-radius: 8px;
}
.grpc-reflect-upstream-info {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.grpc-reflect-upstream-name {
font-size: 13px;
font-weight: 500;
color: var(--text-primary);
}
.grpc-reflect-upstream-url {
font-family: var(--font-mono, monospace);
font-size: 11px;
color: var(--text-muted);
}
.grpc-reflect-status {
display: flex;
align-items: center;
gap: 10px;
font-size: 13px;
color: var(--text-secondary);
}
.grpc-reflect-spinner {
width: 14px;
height: 14px;
border: 2px solid var(--border);
border-top-color: var(--accent);
border-radius: 50%;
animation: grpc-spin 0.7s linear infinite;
flex-shrink: 0;
}
@keyframes grpc-spin { to { transform: rotate(360deg); } }
.grpc-reflect-error-row {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--error, #f87171);
}
-55
View File
@@ -21,17 +21,6 @@
"target_url": "https://api.acme.com/v1/users/{id}",
"created_at": "2026-03-18"
},
{
"id": "op_03",
"name": "search_knowledge_base",
"display_name": "Search Knowledge Base",
"protocol": "graphql",
"method": null,
"status": "active",
"category": "Search",
"target_url": "https://graph.notion-int.acme.com/graphql",
"created_at": "2026-03-14"
},
{
"id": "op_04",
"name": "send_slack_message",
@@ -43,17 +32,6 @@
"target_url": "https://slack.com/api/chat.postMessage",
"created_at": "2026-03-10"
},
{
"id": "op_05",
"name": "stream_telemetry",
"display_name": "Stream Telemetry Events",
"protocol": "grpc",
"method": null,
"status": "draft",
"category": "Observability",
"target_url": "grpc://telemetry.internal.acme.com:9090",
"created_at": "2026-03-05"
},
{
"id": "op_06",
"name": "update_deal_stage",
@@ -65,28 +43,6 @@
"target_url": "https://api.acme.com/v2/crm/deals/{id}",
"created_at": "2026-02-28"
},
{
"id": "op_07",
"name": "fetch_invoice",
"display_name": "Fetch Invoice",
"protocol": "rest",
"method": "GET",
"status": "active",
"category": "Billing",
"target_url": "https://billing.acme.com/v1/invoices/{id}",
"created_at": "2026-02-20"
},
{
"id": "op_08",
"name": "list_products",
"display_name": "List Products",
"protocol": "graphql",
"method": null,
"status": "active",
"category": "Catalog",
"target_url": "https://shop.acme.com/graphql",
"created_at": "2026-02-15"
},
{
"id": "op_09",
"name": "create_support_ticket",
@@ -109,17 +65,6 @@
"target_url": "https://orders.acme.com/v2/status/{id}",
"created_at": "2026-02-05"
},
{
"id": "op_11",
"name": "sync_contacts",
"display_name": "Sync Contacts",
"protocol": "grpc",
"method": null,
"status": "active",
"category": "CRM",
"target_url": "grpc://contacts.internal.acme.com:9091",
"created_at": "2026-01-28"
},
{
"id": "op_12",
"name": "send_email_campaign",
-5
View File
@@ -215,11 +215,6 @@
<button class="btn-primary" id="settings-password-save-btn" type="button" data-i18n="settings.security.change_password">Change password</button>
</div>
</div>
<div data-slot="settings.sso_panel"></div>
<div data-slot="settings.totp_panel"></div>
<div data-slot="settings.audit_panel"></div>
<div data-slot="settings.billing_panel"></div>
</div>
<div class="section-card" style="margin-bottom: 20px;">
-15
View File
@@ -34,11 +34,6 @@
</div>
</div>
<div data-slot="wizard.protocol_cards.graphql"></div>
<div data-slot="wizard.protocol_cards.grpc"></div>
<div data-slot="wizard.protocol_cards.soap"></div>
<div data-slot="wizard.protocol_cards.websocket"></div>
</div><!-- /protocol-grid -->
@@ -54,16 +49,6 @@
</div>
</div>
</div>
<div class="info-callout" id="wizard-edition-protocol-note" hidden>
<svg class="info-callout-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<circle cx="8" cy="8" r="7"/>
<path d="M8 11V8M8 5v-.5"/>
</svg>
<div class="info-callout-body">
<div class="info-callout-title" data-i18n="wizard.step1.community_protocol_title">Ограничение Community версии</div>
<div class="info-callout-text" id="wizard-edition-protocol-note-text"></div>
</div>
</div>
</div><!-- /step-pane-1 -->
-12
View File
@@ -1,12 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<defs><symbol id="icon" viewBox="0 0 24 24">
<path fill="none" stroke="#bc8cff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" d="M12 2l9.5 5.5v11L12 22l-9.5-5.5v-11L12 2z"/>
<circle cx="12" cy="2" r="1.5" fill="#bc8cff"/>
<circle cx="21.5" cy="7.5" r="1.5" fill="#bc8cff"/>
<circle cx="21.5" cy="16.5" r="1.5" fill="#bc8cff"/>
<circle cx="12" cy="22" r="1.5" fill="#bc8cff"/>
<circle cx="2.5" cy="16.5" r="1.5" fill="#bc8cff"/>
<circle cx="2.5" cy="7.5" r="1.5" fill="#bc8cff"/>
</symbol></defs>
<use href="#icon"/>
</svg>

Before

Width:  |  Height:  |  Size: 638 B

-8
View File
@@ -1,8 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<defs><symbol id="icon" viewBox="0 0 24 24">
<rect x="2" y="3" width="8" height="18" rx="2" fill="none" stroke="#0d9488" stroke-width="1.8"/>
<rect x="14" y="3" width="8" height="18" rx="2" fill="none" stroke="#0d9488" stroke-width="1.8"/>
<path fill="none" stroke="#0d9488" stroke-width="1.8" stroke-linecap="round" d="M10 8h4M10 12h4M10 16h4"/>
</symbol></defs>
<use href="#icon"/>
</svg>

Before

Width:  |  Height:  |  Size: 470 B

-3
View File
@@ -319,9 +319,6 @@
getUsageOverview: function(workspaceId, params) {
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/usage' + query(params));
},
getProtocolCapabilities: function(workspaceId) {
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/protocol-capabilities');
},
getOperationUsage: function(workspaceId, operationId, params) {
return get(
'/workspaces/' + encodeURIComponent(workspaceId) + '/usage/operations/' + encodeURIComponent(operationId) + query(params)
-134
View File
@@ -371,16 +371,10 @@ var TRANSLATIONS = {
'settings.security.saved': 'Password updated.',
'settings.security.save_error': 'Failed to change password',
'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',
@@ -431,10 +425,6 @@ var TRANSLATIONS = {
'workspace.switch_error': 'Failed to switch workspace',
'workspace.switch_error_title': 'Workspace switch failed',
'workspace_setup.custom_protocol.rest': 'REST',
'workspace_setup.custom_protocol.graphql': 'GraphQL',
'workspace_setup.custom_protocol.grpc': 'gRPC',
'workspace_setup.custom_protocol.websocket': 'WebSocket',
'workspace_setup.custom_protocol.soap': 'SOAP',
// Wizard
'wizard.back_catalog': 'Operations',
@@ -465,23 +455,13 @@ var TRANSLATIONS = {
'wizard.step1.subtitle': 'Select the transport protocol your upstream service uses. This determines how Crank will communicate with your API and which configuration options are available in subsequent steps.',
'wizard.step1.rest_name': 'REST / HTTP',
'wizard.step1.rest_tagline': 'Standard HTTP verbs over a RESTful endpoint. The most broadly supported option.',
'wizard.step1.graphql_name': 'GraphQL',
'wizard.step1.graphql_tagline': 'Query and mutate a GraphQL schema. Supports queries and mutations over one fixed operation.',
'wizard.step1.grpc_name': 'gRPC',
'wizard.step1.grpc_tagline': 'High-performance binary RPC over HTTP/2 for unary calls to low-latency internal services.',
'wizard.step1.websocket_name': 'WebSocket',
'wizard.step1.websocket_tagline': 'Stateful event streams and push-oriented integrations normalized into bounded MCP streaming tools.',
'wizard.step1.soap_name': 'SOAP',
'wizard.step1.soap_tagline': 'Enterprise WSDL/XSD service contracts normalized into MCP tools with structured XML mapping.',
'wizard.step1.query': 'Query',
'wizard.step1.mutation': 'Mutation',
'wizard.step1.unary': 'Unary',
'wizard.step1.tip_title': 'You can change this later',
'wizard.step1.tip_body': 'Switching protocol after configuring subsequent steps will clear schema and mapping data. Save your operation as a draft before experimenting. See protocol migration guide for details.',
'wizard.step1.community_protocol_title': 'Community protocol scope',
'wizard.step1.community_protocol_note': 'This Community build hides {protocols}. Commercial editions unlock those protocol surfaces.',
'wizard.step5.streaming_title': 'Streaming execution in Community',
'wizard.step5.community_streaming_note': 'This Community build keeps operations in unary mode. Window, session and async job tool families require a commercial edition.',
'wizard.step2.title': 'Select upstream & endpoint',
'wizard.step2.subtitle': 'Choose an existing upstream or register a new one. Then define the specific endpoint path this operation will call.',
'wizard.required': 'Required',
@@ -537,7 +517,6 @@ var TRANSLATIONS = {
'wizard.step2.path_template': 'Path template',
'wizard.step2.path_hint': 'Appended to the API host Base URL. Use {param} for path parameters.',
'wizard.step3.rest.label': 'Request config',
'wizard.step3.graphql.label': 'GQL operation',
'wizard.step3.rest.title': 'HTTP method & format',
'wizard.step3.rest.subtitle': 'Choose the HTTP method and request format. Crank converts MCP tool parameters into the API request body.',
'wizard.step3.rest.method_title': 'HTTP method',
@@ -553,16 +532,6 @@ var TRANSLATIONS = {
'wizard.step3.rest.content_type_hint': 'How the request body is encoded before sending it to the API.',
'wizard.step3.rest.accept': 'Accept',
'wizard.step3.rest.accept_hint': 'Response types accepted from the API server.',
'wizard.step3.graphql.title': 'GraphQL operation',
'wizard.step3.graphql.subtitle': 'Define the GraphQL operation this tool will execute. All requests are sent as HTTP POST to the endpoint configured in the previous step. Variables are populated from MCP tool arguments at runtime.',
'wizard.step3.graphql.type_title': 'Operation type',
'wizard.step3.graphql.type_subtitle': 'GraphQL operation kind — subscriptions are not supported',
'wizard.step3.graphql.query_desc': 'Fetch data without side-effects',
'wizard.step3.graphql.mutation_desc': 'Create, update or delete data',
'wizard.step3.graphql.doc_title': 'Query document',
'wizard.step3.graphql.doc_subtitle': 'GraphQL document with typed variable declarations',
'wizard.step3.graphql.variables_title': 'Variables are mapped in step 5',
'wizard.step3.graphql.variables_body': 'Declare all variables in the query document using the $variable: Type syntax. In step 5 you will map MCP input fields to these variables. Crank serializes the final {"query","variables"} payload automatically.',
'wizard.step4.title': 'Tool config',
'wizard.step4.subtitle': 'Name your tool, write the LLM-facing description, and define the input/output schemas. The description is the primary signal the model uses when deciding whether to call this tool.',
'wizard.step4.identity_title': 'Tool parameters',
@@ -679,23 +648,6 @@ var TRANSLATIONS = {
'wizard.test.window_truncated_note': 'Results were truncated.',
'wizard.test.window_more_note': 'More data is available if you continue from the streaming views.',
'wizard.test.window_incomplete_note': 'The upstream stream ended before the configured window was fully collected.',
'wizard.test.websocket_window_completed': 'WebSocket window test completed',
'wizard.test.websocket_window_completed_body': 'Collected {items} WebSocket event(s) inside the bounded test window.',
'wizard.test.websocket_window_truncated_note': 'The bounded window stopped after reaching the configured item or byte limit.',
'wizard.test.websocket_window_more_note': 'The socket produced more events than this test window retained. Increase limits or switch to session mode if you need a longer stream.',
'wizard.test.websocket_window_incomplete_note': 'The upstream socket closed before the bounded window finished collecting all configured events.',
'wizard.test.websocket_session_started': 'WebSocket session test started',
'wizard.test.websocket_session_started_body': 'WebSocket session {session_id} was created. Continue from Stream sessions after a short delay.',
'wizard.test.websocket_async_job_started': 'WebSocket async job test started',
'wizard.test.websocket_async_job_started_body': 'WebSocket async job {job_id} was created. Continue from the async jobs view for status and result.',
'wizard.test.websocket_failed': 'WebSocket test returned errors',
'wizard.test.websocket_failed_body': 'The socket test failed before Crank could collect a bounded result window.',
'wizard.test.websocket_failed_reconnect_body': 'The upstream socket closed before enough events were collected, and the configured reconnect policy was exhausted.',
'wizard.test.websocket_failed_closed_body': 'The upstream socket closed before the bounded test window completed.',
'wizard.test.session_started': 'Session test started',
'wizard.test.session_started_body': 'Session {session_id} was created. Check Stream sessions in a few seconds.',
'wizard.test.async_job_started': 'Async job test started',
'wizard.test.async_job_started_body': 'Async job {job_id} was created. Track status and result from the async jobs view.',
'wizard.test.no_response': 'No test response yet',
'wizard.test.no_response_body': 'Run a test first, then copy the response into the output example.',
'wizard.test.response_copied': 'Response copied',
@@ -712,18 +664,6 @@ var TRANSLATIONS = {
'wizard.yaml.loaded_body': 'The YAML configuration was loaded.',
'wizard.publish.done': 'Operation published',
'wizard.publish.done_body': 'Version {version} is now published and can be bound into agents.',
'wizard.grpc.discovery_title': 'gRPC discovery moved to live descriptor flow',
'wizard.grpc.discovery_body': 'Server reflection is not wired in this deployment. Save the draft, then use the descriptor-set tools in Step 5 to upload a real descriptor set and discover unary methods from the backend.',
'wizard.grpc.no_methods': 'No unary RPC methods found.',
'wizard.grpc.fields_unresolved': 'Fields not resolved.',
'wizard.grpc.fields_unresolved_body': 'Import or external types are not expanded client-side.',
'wizard.grpc.services_found': 'Found {services_label} and {methods_label}.',
'wizard.grpc.services_label.one': '{count} service',
'wizard.grpc.services_label.other': '{count} services',
'wizard.grpc.methods_label.one': '{count} unary method',
'wizard.grpc.methods_label.other': '{count} unary methods',
'wizard.grpc.services_discovered': '{services} services · {methods} unary methods discovered',
'wizard.grpc.services_none': 'No unary methods discovered yet',
// Common buttons
'btn.save': 'Save changes',
@@ -827,15 +767,10 @@ var TRANSLATIONS = {
// Demo content
'demo.agent.revops-copilot.display_name': 'RevOps Copilot',
'demo.agent.revops-copilot.description': 'Revenue operations assistant with CRM and billing tools.',
'demo.agent.support-triage.display_name': 'Support Triage',
'demo.agent.support-triage.description': 'Draft support agent focused on unary gRPC workflows.',
'demo.operation.crm_create_lead.display_name': 'Create CRM Lead',
'demo.operation.crm_create_lead.description': 'Create a lead record in the CRM system.',
'demo.operation.billing_get_invoice.display_name': 'Get Invoice Status',
'demo.operation.billing_get_invoice.description': 'Fetch invoice state and amount from billing.',
'demo.operation.support_lookup_ticket.display_name': 'Lookup Support Ticket',
'demo.operation.support_lookup_ticket.description': 'Draft unary gRPC support lookup using a descriptor set.',
'demo.operation.marketing_archive_contact.display_name': 'Archive Marketing Contact',
'demo.operation.marketing_archive_contact.description': 'Legacy archived flow kept for audit only.',
@@ -1222,16 +1157,10 @@ var TRANSLATIONS = {
'settings.security.saved': 'Пароль обновлен.',
'settings.security.save_error': 'Не удалось изменить пароль',
'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': 'Текущая сессия',
@@ -1282,10 +1211,6 @@ var TRANSLATIONS = {
'workspace.switch_error': 'Не удалось переключить воркспейс',
'workspace.switch_error_title': 'Не удалось переключить воркспейс',
'workspace_setup.custom_protocol.rest': 'REST',
'workspace_setup.custom_protocol.graphql': 'GraphQL',
'workspace_setup.custom_protocol.grpc': 'gRPC',
'workspace_setup.custom_protocol.websocket': 'WebSocket',
'workspace_setup.custom_protocol.soap': 'SOAP',
// Wizard
'wizard.back_catalog': 'Операции',
@@ -1316,23 +1241,13 @@ var TRANSLATIONS = {
'wizard.step1.subtitle': 'Выберите протокол вашего API. От этого зависит, как Crank будет выполнять запросы и какие настройки будут доступны на следующих шагах.',
'wizard.step1.rest_name': 'REST / HTTP',
'wizard.step1.rest_tagline': 'Стандартные HTTP-методы для REST API. Самый универсальный вариант.',
'wizard.step1.graphql_name': 'GraphQL',
'wizard.step1.graphql_tagline': 'Операции по GraphQL-схеме. Поддерживаются Query и Mutation для одной фиксированной операции.',
'wizard.step1.grpc_name': 'gRPC',
'wizard.step1.grpc_tagline': 'Бинарный RPC поверх HTTP/2 для быстрых внутренних сервисов.',
'wizard.step1.websocket_name': 'WebSocket',
'wizard.step1.websocket_tagline': 'Долгие соединения и потоковые события, оформленные как MCP инструменты.',
'wizard.step1.soap_name': 'SOAP',
'wizard.step1.soap_tagline': 'SOAP-сервисы по WSDL/XSD, оформленные как MCP инструменты.',
'wizard.step1.query': 'Query',
'wizard.step1.mutation': 'Mutation',
'wizard.step1.unary': 'Unary',
'wizard.step1.tip_title': 'Это можно изменить позже',
'wizard.step1.tip_body': 'Если сменить протокол после настройки следующих шагов, схемы и связи полей будут очищены. Перед экспериментами сохраните операцию.',
'wizard.step1.community_protocol_title': 'Граница протоколов Community',
'wizard.step1.community_protocol_note': 'Ограничение Community версии. В редакции Community протоколы {protocols} недоступны. Если вы заинтересованы в работе с данными протоколами, обратитесь к разработчикам для приобретения коммерческой версии или воспользуйтесь облачным решением.',
'wizard.step5.streaming_title': 'Потоковое выполнение',
'wizard.step5.community_streaming_note': 'В Community операции выполняются в обычном режиме запрос-ответ. Потоковые сценарии, сессии и фоновые задания требуют коммерческую редакцию.',
'wizard.step2.title': 'Выберите API-хост и путь',
'wizard.step2.subtitle': 'Выберите существующий API-хост или добавьте новый. Затем задайте путь, который будет вызывать эта операция.',
'wizard.required': 'Обязательно',
@@ -1388,7 +1303,6 @@ var TRANSLATIONS = {
'wizard.step2.path_template': 'Шаблон пути',
'wizard.step2.path_hint': 'Добавляется к базовому URL API-хоста. Используйте {param} для параметров пути.',
'wizard.step3.rest.label': 'Настройки запроса',
'wizard.step3.graphql.label': 'GQL операция',
'wizard.step3.rest.title': 'HTTP метод и формат',
'wizard.step3.rest.subtitle': 'Выберите HTTP-метод и формат запроса. Crank подставит параметры MCP инструмента в запрос к API.',
'wizard.step3.rest.method_title': 'HTTP метод',
@@ -1404,16 +1318,6 @@ var TRANSLATIONS = {
'wizard.step3.rest.content_type_hint': 'В каком формате отправлять данные в API.',
'wizard.step3.rest.accept': 'Accept',
'wizard.step3.rest.accept_hint': 'Какой формат ответа ожидать от API.',
'wizard.step3.graphql.title': 'GraphQL операция',
'wizard.step3.graphql.subtitle': 'Определите GraphQL-операцию, которую будет выполнять этот инструмент. Все запросы отправляются как HTTP POST на путь, настроенный на предыдущем шаге. Переменные заполняются из аргументов MCP инструмента во время выполнения.',
'wizard.step3.graphql.type_title': 'Тип операции',
'wizard.step3.graphql.type_subtitle': 'Вид GraphQL-операции. Подписки не поддерживаются.',
'wizard.step3.graphql.query_desc': 'Получение данных без побочных эффектов',
'wizard.step3.graphql.mutation_desc': 'Создание, изменение или удаление данных',
'wizard.step3.graphql.doc_title': 'Документ запроса',
'wizard.step3.graphql.doc_subtitle': 'GraphQL-документ с типизированными объявлениями переменных',
'wizard.step3.graphql.variables_title': 'Переменные связываются на шаге 5',
'wizard.step3.graphql.variables_body': 'Объявите все переменные в документе запроса в формате $variable: Type. На шаге 5 вы свяжете поля MCP инструмента с этими переменными.',
'wizard.step4.title': 'Настройки инструмента',
'wizard.step4.subtitle': 'Задайте имя инструмента, описание для LLM и определите входную/выходную схему. Описание — главный сигнал, по которому модель решает, вызывать ли этот инструмент.',
'wizard.step4.identity_title': 'Параметры инструмента',
@@ -1530,23 +1434,6 @@ var TRANSLATIONS = {
'wizard.test.window_truncated_note': 'Результат был усечен.',
'wizard.test.window_more_note': 'Доступно продолжение, если перейти к streaming-представлениям.',
'wizard.test.window_incomplete_note': 'Upstream-поток завершился раньше, чем удалось собрать всё окно.',
'wizard.test.websocket_window_completed': 'WebSocket оконный тест завершен',
'wizard.test.websocket_window_completed_body': 'В пределах ограниченного тестового окна собрано {items} WebSocket event(s).',
'wizard.test.websocket_window_truncated_note': 'Ограниченное окно остановилось после достижения лимита по элементам или байтам.',
'wizard.test.websocket_window_more_note': 'Сокет выдал больше событий, чем сохранило это тестовое окно. Увеличьте лимиты или переключитесь в session mode, если нужен более длинный поток.',
'wizard.test.websocket_window_incomplete_note': 'Upstream WebSocket закрылся до того, как bounded окно успело собрать все настроенные события.',
'wizard.test.websocket_session_started': 'WebSocket session-тест запущен',
'wizard.test.websocket_session_started_body': 'WebSocket session {session_id} создана. Продолжайте в разделе Stream sessions через короткую паузу.',
'wizard.test.websocket_async_job_started': 'WebSocket async job тест запущен',
'wizard.test.websocket_async_job_started_body': 'Создан WebSocket async job {job_id}. Дальше отслеживайте статус и результат в представлении async jobs.',
'wizard.test.websocket_failed': 'WebSocket тест завершился с ошибками',
'wizard.test.websocket_failed_body': 'Тест сокета завершился до того, как Crank успел собрать bounded result window.',
'wizard.test.websocket_failed_reconnect_body': 'Upstream WebSocket закрылся раньше, чем удалось собрать достаточно событий, и настроенная reconnect policy была исчерпана.',
'wizard.test.websocket_failed_closed_body': 'Upstream WebSocket закрылся до завершения bounded test window.',
'wizard.test.session_started': 'Session-тест запущен',
'wizard.test.session_started_body': 'Сессия {session_id} создана. Проверьте статус в разделе Stream sessions через несколько секунд.',
'wizard.test.async_job_started': 'Async job тест запущен',
'wizard.test.async_job_started_body': 'Создан async job {job_id}. Отслеживайте статус и результат через представление async jobs.',
'wizard.test.no_response': 'Тестового ответа пока нет',
'wizard.test.no_response_body': 'Сначала выполните тест, затем скопируйте ответ в выходной пример.',
'wizard.test.response_copied': 'Ответ скопирован',
@@ -1563,22 +1450,6 @@ var TRANSLATIONS = {
'wizard.yaml.loaded_body': 'YAML-конфигурация загружена.',
'wizard.publish.done': 'Операция опубликована',
'wizard.publish.done_body': 'Версия {version} опубликована и теперь может быть привязана к агентам.',
'wizard.grpc.discovery_title': 'gRPC discovery переведен в live descriptor flow',
'wizard.grpc.discovery_body': 'Server reflection в этом развертывании не подключен. Сохраните операцию и используйте descriptor set на шаге 5, чтобы загрузить описание сервиса и открыть unary methods.',
'wizard.grpc.no_methods': 'Unary RPC methods не найдены.',
'wizard.grpc.fields_unresolved': 'Поля не разрешены.',
'wizard.grpc.fields_unresolved_body': 'Import-ы или внешние типы не разворачиваются на стороне клиента.',
'wizard.grpc.services_found': 'Найдено {services_label} и {methods_label}.',
'wizard.grpc.services_label.one': '{count} сервис',
'wizard.grpc.services_label.few': '{count} сервиса',
'wizard.grpc.services_label.many': '{count} сервисов',
'wizard.grpc.services_label.other': '{count} сервиса',
'wizard.grpc.methods_label.one': '{count} unary-метод',
'wizard.grpc.methods_label.few': '{count} unary-метода',
'wizard.grpc.methods_label.many': '{count} unary-методов',
'wizard.grpc.methods_label.other': '{count} unary-метода',
'wizard.grpc.services_discovered': 'Сервисов открыто: {services} · unary методов: {methods}',
'wizard.grpc.services_none': 'Unary methods пока не открыты',
// Common buttons
'btn.save': 'Сохранить',
@@ -1682,15 +1553,10 @@ var TRANSLATIONS = {
// Demo content
'demo.agent.revops-copilot.display_name': 'Помощник RevOps',
'demo.agent.revops-copilot.description': 'Ассистент Revenue Operations с CRM- и billing-инструментами.',
'demo.agent.support-triage.display_name': 'Триаж поддержки',
'demo.agent.support-triage.description': 'Черновой агент поддержки, сфокусированный на unary gRPC-сценариях.',
'demo.operation.crm_create_lead.display_name': 'Создать CRM-лид',
'demo.operation.crm_create_lead.description': 'Создает запись лида в CRM-системе.',
'demo.operation.billing_get_invoice.display_name': 'Статус инвойса',
'demo.operation.billing_get_invoice.description': 'Получает состояние и сумму инвойса из billing-системы.',
'demo.operation.support_lookup_ticket.display_name': 'Найти тикет поддержки',
'demo.operation.support_lookup_ticket.description': 'Черновой unary gRPC lookup поддержки через descriptor set.',
'demo.operation.marketing_archive_contact.display_name': 'Архивировать маркетинговый контакт',
'demo.operation.marketing_archive_contact.description': 'Устаревший архивный сценарий, оставленный только для аудита.',
+1 -11
View File
@@ -1,15 +1,5 @@
(function() {
var allowedSlots = {
'settings.sso_panel': true,
'settings.totp_panel': true,
'settings.audit_panel': true,
'settings.billing_panel': true,
'wizard.protocol_cards.graphql': true,
'wizard.protocol_cards.grpc': true,
'wizard.protocol_cards.soap': true,
'wizard.protocol_cards.websocket': true,
};
var allowedSlots = {};
var handlers = Object.create(null);
function noop() {}
-2
View File
@@ -53,8 +53,6 @@ var REQUEST_MAPPING_TARGET_PREFIXES = [
'query',
'headers',
'body',
'variables',
'grpc',
];
function normalizeInputSource(path) {
+8 -45
View File
@@ -208,56 +208,19 @@
});
}
function applyEditionProtocolVisibility(capabilities) {
var supported = capabilities && Array.isArray(capabilities.supported_protocols)
? capabilities.supported_protocols.slice()
: ['rest'];
var knownProtocols = ['rest', 'graphql', 'grpc', 'websocket', 'soap'];
function applyEditionProtocolVisibility(_capabilities) {
window.wizardProtocol = 'rest';
document.querySelectorAll('.protocol-card').forEach(function(card) {
var protocol = card.dataset.protocol || 'rest';
var isSupported = supported.indexOf(protocol) >= 0;
card.hidden = !isSupported;
card.setAttribute('aria-disabled', isSupported ? 'false' : 'true');
if (!isSupported) {
card.classList.remove('selected');
card.setAttribute('aria-checked', 'false');
}
var isRest = (card.dataset.protocol || 'rest') === 'rest';
card.hidden = !isRest;
card.setAttribute('aria-disabled', isRest ? 'false' : 'true');
card.classList.toggle('selected', isRest);
card.setAttribute('aria-checked', isRest ? 'true' : 'false');
});
var hiddenProtocols = knownProtocols.filter(function(protocol) {
return supported.indexOf(protocol) === -1;
});
if (supported.indexOf(window.wizardProtocol) === -1) {
window.wizardProtocol = supported[0] || 'rest';
}
var selectedCard = document.querySelector('.protocol-card[data-protocol="' + window.wizardProtocol + '"]');
if (selectedCard) {
selectedCard.classList.add('selected');
selectedCard.setAttribute('aria-checked', 'true');
}
var step3Name = document.getElementById('sidebar-step-3-name');
var labels = window.CrankWizardState.step3Labels();
if (step3Name) {
step3Name.textContent = labels[window.wizardProtocol] || tKey('wizard.step3.rest.label');
}
var note = document.getElementById('wizard-edition-protocol-note');
var noteText = document.getElementById('wizard-edition-protocol-note-text');
if (note && noteText) {
if (hiddenProtocols.length) {
note.hidden = false;
noteText.textContent = tfKey('wizard.step1.community_protocol_note', {
protocols: hiddenProtocols.map(function(protocol) {
return tKey('settings.capability.' + protocol);
}).join(', ')
});
} else {
note.hidden = true;
}
step3Name.textContent = tKey('wizard.step3.rest.label');
}
}
+1 -18
View File
@@ -18,8 +18,6 @@
wizardAuthProfiles: [],
selectedUpstreamId: null,
editingUpstreamId: null,
protoParsed: null,
selectedRpcMethod: null,
};
Object.keys(defaults).forEach(function(key) {
@@ -71,22 +69,7 @@
}
async function loadProtocolCapabilities() {
if (!window.wizardWorkspaceId || !window.CrankApi || typeof window.CrankApi.getProtocolCapabilities !== 'function') {
window.wizardProtocolCapabilities = defaultProtocolCapabilities();
return window.wizardProtocolCapabilities;
}
try {
var response = await window.CrankApi.getProtocolCapabilities(window.wizardWorkspaceId);
var mapped = {};
(response.items || []).forEach(function(item) {
mapped[item.protocol] = item;
});
window.wizardProtocolCapabilities = mapped;
} catch (_error) {
window.wizardProtocolCapabilities = defaultProtocolCapabilities();
}
window.wizardProtocolCapabilities = defaultProtocolCapabilities();
return window.wizardProtocolCapabilities;
}
-22
View File
@@ -26,18 +26,12 @@ var wizardEditId = window.wizardEditId;
var wizardWorkspaceId = window.wizardWorkspaceId;
var wizardCurrentOperation = window.wizardCurrentOperation;
var wizardCurrentVersion = window.wizardCurrentVersion;
var wizardProtoUpload = window.wizardProtoUpload;
var wizardDescriptorSetUpload = window.wizardDescriptorSetUpload;
var wizardSoapWsdlUpload = window.wizardSoapWsdlUpload;
var wizardSoapXsdUpload = window.wizardSoapXsdUpload;
var wizardTestResponsePreview = window.wizardTestResponsePreview;
var wizardProtocolCapabilities = window.wizardProtocolCapabilities;
var wizardSecrets = window.wizardSecrets;
var wizardAuthProfiles = window.wizardAuthProfiles;
var selectedUpstreamId = window.selectedUpstreamId;
var editingUpstreamId = window.editingUpstreamId;
var protoParsed = window.protoParsed;
var selectedRpcMethod = window.selectedRpcMethod;
async function initWizardPage() {
renderSidebarBrand('create');
@@ -252,22 +246,6 @@ function selectMethod(btn) {
}
}
/* ── GraphQL operation type picker (Step 4 GraphQL) ── */
function selectGqlType(btn) {
document.querySelectorAll('.gql-type-card').forEach(function(b) { b.classList.remove('active'); });
btn.classList.add('active');
// Update query editor placeholder if it's still the default
var editor = document.getElementById('gql-query-editor');
if (!editor) return;
var type = btn.dataset.gqlType;
var defaults = {
query: 'query GetContact($id: ID!) {\n contact(id: $id) {\n id\n firstName\n lastName\n email\n company {\n id\n name\n }\n createdAt\n }\n}',
mutation: 'mutation CreateContact(\n $firstName: String!\n $lastName: String!\n $email: String!\n $company: String\n) {\n createContact(input: {\n firstName: $firstName\n lastName: $lastName\n email: $email\n company: $company\n }) {\n id\n firstName\n lastName\n createdAt\n }\n}'
};
if (defaults[type]) editor.value = defaults[type];
}
function escapeHtml(str) {
return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
-1
View File
@@ -10,7 +10,6 @@ ADMIN_PORT="${CRANK_E2E_ADMIN_PORT:-3301}"
MCP_PORT="${CRANK_E2E_MCP_PORT:-3302}"
UI_PORT="${CRANK_E2E_UI_PORT:-3300}"
STREAM_FIXTURE_PORT="${CRANK_E2E_STREAM_FIXTURE_PORT:-3310}"
GRPC_FIXTURE_BIND="${CRANK_E2E_GRPC_FIXTURE_BIND:-127.0.0.1:3311}"
POSTGRES_DB="${CRANK_E2E_POSTGRES_DB:-crank}"
POSTGRES_USER="${CRANK_E2E_POSTGRES_USER:-crank}"
POSTGRES_PASSWORD="${CRANK_E2E_POSTGRES_PASSWORD:-crank}"
-4
View File
@@ -13,10 +13,6 @@ test('shell and wizard expose stable diagnostics hooks', async ({ page }) => {
await page.goto('/wizard/');
await expect(page.locator('[data-testid="wizard-protocol-grid"]')).toBeVisible();
await expect(page.locator('[data-testid="wizard-protocol-rest"]')).toBeVisible();
await expect(page.locator('[data-testid="wizard-protocol-graphql"]')).toBeHidden();
await expect(page.locator('[data-testid="wizard-protocol-grpc"]')).toBeHidden();
await expect(page.locator('[data-testid="wizard-protocol-websocket"]')).toBeHidden();
await expect(page.locator('[data-testid="wizard-protocol-soap"]')).toBeHidden();
await expect(page.locator('html')).toHaveAttribute('data-crank-bootstrap-state', 'ready');
});
+1 -8
View File
@@ -22,17 +22,10 @@ test('wizard loads and protocol selection updates flow', async ({ page }) => {
await expect(page.locator('#step-panel-2 .step-panel-title')).toContainText(localized('Select upstream', 'Выберите upstream'));
});
test('community wizard hides premium protocols and explains edition scope', async ({ page }) => {
test('community wizard exposes REST-only protocol flow', async ({ page }) => {
await login(page);
await page.goto('/wizard/');
await expect(page.locator('[data-testid="wizard-protocol-rest"]')).toBeVisible();
await expect(page.locator('[data-testid="wizard-protocol-graphql"]')).toBeHidden();
await expect(page.locator('[data-testid="wizard-protocol-grpc"]')).toBeHidden();
await expect(page.locator('[data-testid="wizard-protocol-websocket"]')).toBeHidden();
await expect(page.locator('[data-testid="wizard-protocol-soap"]')).toBeHidden();
await expect(page.locator('#wizard-edition-protocol-note')).toContainText(
localized('Commercial editions unlock', 'коммерческих редакциях')
);
await page.locator('[data-testid="wizard-protocol-rest"]').click();
await page.locator('#btn-continue').click();
-6
View File
@@ -989,24 +989,18 @@ fn credential_allows_security_level(
fn security_level_rank(level: OperationSecurityLevel) -> u8 {
match level {
OperationSecurityLevel::Standard => 0,
OperationSecurityLevel::Elevated => 1,
OperationSecurityLevel::Strict => 2,
}
}
fn serialize_security_level(level: OperationSecurityLevel) -> &'static str {
match level {
OperationSecurityLevel::Standard => "standard",
OperationSecurityLevel::Elevated => "elevated",
OperationSecurityLevel::Strict => "strict",
}
}
fn serialize_machine_access_mode(mode: crank_core::MachineAccessMode) -> &'static str {
match mode {
crank_core::MachineAccessMode::StaticAgentKey => "static_agent_key",
crank_core::MachineAccessMode::ShortLivedToken => "short_lived_token",
crank_core::MachineAccessMode::OneTimeToken => "one_time_token",
}
}
+2 -100
View File
@@ -2,8 +2,7 @@ use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use crate::{
edition::{MachineAccessMode, OperationSecurityLevel},
ids::{AgentId, AuthProfileId, OperationId, SecretId, WorkspaceId},
ids::{AuthProfileId, SecretId, WorkspaceId},
protocol::AuthKind,
};
@@ -64,55 +63,13 @@ pub struct AuthProfile {
pub updated_at: OffsetDateTime,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AgentTokenGrantType {
AgentKey,
RefreshToken,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct IssueAgentTokenRequest {
pub grant_type: AgentTokenGrantType,
#[serde(skip_serializing_if = "Option::is_none")]
pub agent_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub refresh_token: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub scope: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct IssueOneTimeAgentTokenRequest {
pub agent_key: String,
pub operation_id: OperationId,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub scope: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct IssuedAgentTokenResponse {
pub access_token: String,
pub token_type: String,
pub expires_in: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub refresh_token: Option<String>,
pub machine_access_mode: MachineAccessMode,
pub security_level: OperationSecurityLevel,
pub agent_id: AgentId,
}
#[cfg(test)]
mod tests {
use serde_json::json;
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
use super::{
AgentTokenGrantType, ApiKeyHeaderAuthConfig, AuthConfig, AuthProfile,
IssueAgentTokenRequest, IssueOneTimeAgentTokenRequest, IssuedAgentTokenResponse,
};
use super::{ApiKeyHeaderAuthConfig, AuthConfig, AuthProfile};
use crate::{
edition::{MachineAccessMode, OperationSecurityLevel},
ids::{AuthProfileId, SecretId, WorkspaceId},
protocol::AuthKind,
};
@@ -137,59 +94,4 @@ mod tests {
assert_eq!(value["created_at"], json!("2026-03-25T12:00:00Z"));
assert_eq!(value["updated_at"], json!("2026-03-25T12:05:00Z"));
}
#[test]
fn issue_agent_token_request_serializes_stable_contract() {
let request = IssueAgentTokenRequest {
grant_type: AgentTokenGrantType::AgentKey,
agent_key: Some("crk_agent_secret".to_owned()),
refresh_token: None,
scope: vec!["tools:call".to_owned()],
};
let value = serde_json::to_value(&request).unwrap();
assert_eq!(value["grant_type"], json!("agent_key"));
assert_eq!(value["agent_key"], json!("crk_agent_secret"));
assert_eq!(value["scope"], json!(["tools:call"]));
assert_eq!(value.get("refresh_token"), None);
}
#[test]
fn issue_one_time_agent_token_request_serializes_stable_contract() {
let request = IssueOneTimeAgentTokenRequest {
agent_key: "crk_agent_secret".to_owned(),
operation_id: "op_01".into(),
scope: vec!["tools:call".to_owned()],
};
let value = serde_json::to_value(&request).unwrap();
assert_eq!(value["agent_key"], json!("crk_agent_secret"));
assert_eq!(value["operation_id"], json!("op_01"));
assert_eq!(value["scope"], json!(["tools:call"]));
}
#[test]
fn issued_agent_token_response_serializes_machine_access_metadata() {
let response = IssuedAgentTokenResponse {
access_token: "tok_01".to_owned(),
token_type: "Bearer".to_owned(),
expires_in: 300,
refresh_token: Some("rfr_01".to_owned()),
machine_access_mode: MachineAccessMode::ShortLivedToken,
security_level: OperationSecurityLevel::Elevated,
agent_id: "agt_01".into(),
};
let value = serde_json::to_value(&response).unwrap();
assert_eq!(value["access_token"], json!("tok_01"));
assert_eq!(value["token_type"], json!("Bearer"));
assert_eq!(value["expires_in"], json!(300));
assert_eq!(value["refresh_token"], json!("rfr_01"));
assert_eq!(value["machine_access_mode"], json!("short_lived_token"));
assert_eq!(value["security_level"], json!("elevated"));
assert_eq!(value["agent_id"], json!("agt_01"));
}
}
-6
View File
@@ -6,24 +6,18 @@ use crate::Protocol;
#[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,
}
impl Default for OperationSecurityLevel {
+2 -229
View File
@@ -1,13 +1,9 @@
use std::sync::Arc;
use async_trait::async_trait;
use time::OffsetDateTime;
use crate::{
IssueAgentTokenRequest, IssueOneTimeAgentTokenRequest, IssuedAgentTokenResponse,
MachineAccessMode, Membership, MembershipRole, OperationSecurityLevel, PlatformApiKeyScope,
User, UserId, WorkspaceId,
MachineAccessMode, Membership, OperationSecurityLevel, PlatformApiKeyScope, User, WorkspaceId,
};
use async_trait::async_trait;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VerifiedMachineCredential {
@@ -32,70 +28,6 @@ pub trait MachineCredentialVerifier: Send + Sync {
pub type SharedMachineCredentialVerifier = Arc<dyn MachineCredentialVerifier>;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TokenIssuerActor {
pub user_id: UserId,
pub workspace_id: WorkspaceId,
pub role: MembershipRole,
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum TokenIssuerError {
#[error("machine tokens are not available in this edition")]
NotSupportedInEdition,
#[error("invalid grant: {0}")]
InvalidGrant(String),
#[error("agent key is unknown")]
AgentKeyUnknown,
#[error("operation is not strict")]
OperationNotStrict,
#[error("operation is not published for agent")]
OperationNotPublishedForAgent,
#[error("registry failure: {0}")]
RegistryFailure(String),
#[error("replay guard failure: {0}")]
ReplayGuardFailure(String),
}
#[async_trait]
pub trait MachineTokenIssuer: Send + Sync {
async fn issue_short_lived(
&self,
request: IssueAgentTokenRequest,
actor: &TokenIssuerActor,
) -> Result<IssuedAgentTokenResponse, TokenIssuerError>;
async fn issue_one_time(
&self,
request: IssueOneTimeAgentTokenRequest,
actor: &TokenIssuerActor,
) -> Result<IssuedAgentTokenResponse, TokenIssuerError>;
}
pub type SharedMachineTokenIssuer = Arc<dyn MachineTokenIssuer>;
#[derive(Clone, Copy, Debug, Default)]
pub struct NoMachineTokenIssuer;
#[async_trait]
impl MachineTokenIssuer for NoMachineTokenIssuer {
async fn issue_short_lived(
&self,
_request: IssueAgentTokenRequest,
_actor: &TokenIssuerActor,
) -> Result<IssuedAgentTokenResponse, TokenIssuerError> {
Err(TokenIssuerError::NotSupportedInEdition)
}
async fn issue_one_time(
&self,
_request: IssueOneTimeAgentTokenRequest,
_actor: &TokenIssuerActor,
) -> Result<IssuedAgentTokenResponse, TokenIssuerError> {
Err(TokenIssuerError::NotSupportedInEdition)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum IdentityProviderKind {
Password,
@@ -126,64 +58,8 @@ pub struct LoginPayload {
pub password: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SsoAuthorizeRequest {
pub workspace_id: WorkspaceId,
pub provider_id: String,
pub base_origin: String,
pub return_to: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SsoAuthorizeRedirect {
pub authorization_url: String,
pub state: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SsoCallbackRequest {
pub workspace_id: WorkspaceId,
pub provider_id: String,
pub code: String,
pub base_origin: String,
pub return_to: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TwoFactorPendingIdentity {
pub user_id: UserId,
pub workspace_id: Option<WorkspaceId>,
pub return_to: Option<String>,
pub expires_at: OffsetDateTime,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TwoFactorStatus {
pub enabled: bool,
pub pending_setup: bool,
pub recovery_codes_remaining: usize,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TwoFactorSetup {
pub secret: String,
pub otpauth_url: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TwoFactorActivation {
pub recovery_codes: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct TwoFactorChallenge {
pub code: Option<String>,
pub recovery_code: Option<String>,
}
pub enum LoginOutcome {
Authenticated(AuthenticatedIdentity),
TwoFactorRequired(TwoFactorPendingIdentity),
}
#[async_trait]
@@ -193,109 +69,6 @@ pub trait IdentityProvider: Send + Sync {
fn kind(&self) -> IdentityProviderKind;
async fn login_password(&self, payload: LoginPayload) -> Result<LoginOutcome, IdentityError>;
async fn begin_sso(
&self,
_request: SsoAuthorizeRequest,
) -> Result<SsoAuthorizeRedirect, IdentityError> {
Err(IdentityError::NotSupportedForProvider)
}
async fn complete_sso(
&self,
_request: SsoCallbackRequest,
) -> Result<LoginOutcome, IdentityError> {
Err(IdentityError::NotSupportedForProvider)
}
async fn get_two_factor_status(
&self,
_user_id: &UserId,
) -> Result<TwoFactorStatus, IdentityError> {
Err(IdentityError::NotSupportedForProvider)
}
async fn begin_two_factor_setup(
&self,
_user_id: &UserId,
_email: &str,
) -> Result<TwoFactorSetup, IdentityError> {
Err(IdentityError::NotSupportedForProvider)
}
async fn activate_two_factor(
&self,
_user_id: &UserId,
_code: &str,
) -> Result<TwoFactorActivation, IdentityError> {
Err(IdentityError::NotSupportedForProvider)
}
async fn disable_two_factor(
&self,
_user_id: &UserId,
_challenge: TwoFactorChallenge,
) -> Result<(), IdentityError> {
Err(IdentityError::NotSupportedForProvider)
}
async fn verify_two_factor_login(
&self,
_pending: &TwoFactorPendingIdentity,
_challenge: TwoFactorChallenge,
) -> Result<AuthenticatedIdentity, IdentityError> {
Err(IdentityError::NotSupportedForProvider)
}
}
pub type SharedIdentityProvider = Arc<dyn IdentityProvider>;
#[cfg(test)]
mod tests {
use super::{MachineTokenIssuer, NoMachineTokenIssuer, TokenIssuerActor, TokenIssuerError};
use crate::{
AgentTokenGrantType, IssueAgentTokenRequest, IssueOneTimeAgentTokenRequest, MembershipRole,
UserId, WorkspaceId,
};
fn actor() -> TokenIssuerActor {
TokenIssuerActor {
user_id: UserId::new("user_01"),
workspace_id: WorkspaceId::new("ws_01"),
role: MembershipRole::Owner,
}
}
#[tokio::test]
async fn no_machine_token_issuer_rejects_short_lived_issue() {
let result = NoMachineTokenIssuer
.issue_short_lived(
IssueAgentTokenRequest {
grant_type: AgentTokenGrantType::AgentKey,
agent_key: Some("crk_agent_secret".to_owned()),
refresh_token: None,
scope: vec![],
},
&actor(),
)
.await;
assert_eq!(result, Err(TokenIssuerError::NotSupportedInEdition));
}
#[tokio::test]
async fn no_machine_token_issuer_rejects_one_time_issue() {
let result = NoMachineTokenIssuer
.issue_one_time(
IssueOneTimeAgentTokenRequest {
agent_key: "crk_agent_secret".to_owned(),
operation_id: "op_01".into(),
scope: vec![],
},
&actor(),
)
.await;
assert_eq!(result, Err(TokenIssuerError::NotSupportedInEdition));
}
}
-75
View File
@@ -1,75 +0,0 @@
use std::sync::Arc;
use async_trait::async_trait;
use crate::{TenantId, UsageRollup};
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum BillingGate {
Allow,
Deny { reason: String },
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum BillingError {
#[error("billing integration is not available in this edition")]
NotSupportedInEdition,
#[error("billing provider failure: {0}")]
Provider(String),
}
#[async_trait]
pub trait BillingHook: Send + Sync {
async fn on_rollup(&self, rollup: UsageRollup) -> Result<(), BillingError>;
async fn check_billing_gate(&self, tenant: &TenantId) -> Result<BillingGate, BillingError>;
}
pub type SharedBillingHook = Arc<dyn BillingHook>;
#[derive(Clone, Copy, Debug, Default)]
pub struct NoopBillingHook;
#[async_trait]
impl BillingHook for NoopBillingHook {
async fn on_rollup(&self, _rollup: UsageRollup) -> Result<(), BillingError> {
Ok(())
}
async fn check_billing_gate(&self, _tenant: &TenantId) -> Result<BillingGate, BillingError> {
Ok(BillingGate::Allow)
}
}
#[cfg(test)]
mod tests {
use super::{BillingGate, BillingHook, NoopBillingHook};
use crate::{TenantId, UsagePeriod, UsageRollup, WorkspaceId};
#[tokio::test]
async fn noop_billing_hook_allows_default_gate_and_rollups() {
let hook = NoopBillingHook;
hook.on_rollup(UsageRollup {
workspace_id: WorkspaceId::new("ws_01"),
agent_id: None,
operation_id: None,
period: UsagePeriod::Last24Hours,
calls_total: 10,
calls_ok: 9,
calls_error: 1,
p50_ms: 12,
p95_ms: 42,
p99_ms: 77,
})
.await
.unwrap();
let gate = hook
.check_billing_gate(&TenantId::new("tenant_01"))
.await
.unwrap();
assert_eq!(gate, BillingGate::Allow);
}
}
-2
View File
@@ -1,8 +1,6 @@
pub mod access;
pub mod audit;
pub mod auth;
pub mod billing;
pub mod capability;
pub mod metering;
pub mod protocol;
pub mod tenancy;
-81
View File
@@ -1,81 +0,0 @@
use std::sync::Arc;
use async_trait::async_trait;
use crate::{TenantId, WorkspaceId};
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct TenantResolutionContext {
pub workspace_id: Option<WorkspaceId>,
pub workspace_slug: Option<String>,
pub host: Option<String>,
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum TenancyError {
#[error("tenant lookup is not supported in this edition")]
UnsupportedLookup,
#[error("internal tenancy failure: {0}")]
Internal(String),
}
#[async_trait]
pub trait TenantController: Send + Sync {
fn resolve_tenant(&self, request: &TenantResolutionContext) -> Result<TenantId, TenancyError>;
async fn list_workspaces_for_tenant(
&self,
tenant: &TenantId,
) -> Result<Vec<WorkspaceId>, TenancyError>;
}
pub type SharedTenantController = Arc<dyn TenantController>;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SingleTenantController {
tenant_id: TenantId,
}
impl Default for SingleTenantController {
fn default() -> Self {
Self {
tenant_id: TenantId::new("default"),
}
}
}
#[async_trait]
impl TenantController for SingleTenantController {
fn resolve_tenant(&self, _request: &TenantResolutionContext) -> Result<TenantId, TenancyError> {
Ok(self.tenant_id.clone())
}
async fn list_workspaces_for_tenant(
&self,
_tenant: &TenantId,
) -> Result<Vec<WorkspaceId>, TenancyError> {
Ok(Vec::new())
}
}
#[cfg(test)]
mod tests {
use super::{SingleTenantController, TenantController, TenantResolutionContext};
use crate::TenantId;
#[tokio::test]
async fn single_tenant_controller_returns_default_tenant() {
let controller = SingleTenantController::default();
let tenant = controller
.resolve_tenant(&TenantResolutionContext::default())
.unwrap();
let workspaces = controller
.list_workspaces_for_tenant(&tenant)
.await
.unwrap();
assert_eq!(tenant, TenantId::new("default"));
assert!(workspaces.is_empty());
}
}
-1
View File
@@ -48,7 +48,6 @@ define_id!(ToolId);
define_id!(SampleId);
define_id!(AuthProfileId);
define_id!(WorkspaceId);
define_id!(TenantId);
define_id!(UserId);
define_id!(UserSessionId);
define_id!(AgentId);
+5 -16
View File
@@ -17,9 +17,8 @@ pub use access::{
};
pub use agent::{Agent, AgentOperationBinding, AgentStatus, AgentVersion};
pub use auth::{
AgentTokenGrantType, ApiKeyHeaderAuthConfig, ApiKeyQueryAuthConfig, AuthConfig, AuthProfile,
BasicAuthConfig, BearerAuthConfig, IssueAgentTokenRequest, IssueOneTimeAgentTokenRequest,
IssuedAgentTokenResponse,
ApiKeyHeaderAuthConfig, ApiKeyQueryAuthConfig, AuthConfig, AuthProfile, BasicAuthConfig,
BearerAuthConfig,
};
pub use cache::{
CacheBackend, CacheScope, CacheStoreError, CachedHeader, CachedResponse,
@@ -38,14 +37,8 @@ pub use ext::audit::{
};
pub use ext::auth::{
AuthenticatedIdentity, IdentityError, IdentityProvider, IdentityProviderKind, LoginOutcome,
LoginPayload, MachineCredentialVerifier, MachineCredentialVerifierError, MachineTokenIssuer,
NoMachineTokenIssuer, SharedIdentityProvider, SharedMachineCredentialVerifier,
SharedMachineTokenIssuer, SsoAuthorizeRedirect, SsoAuthorizeRequest, SsoCallbackRequest,
TokenIssuerActor, TokenIssuerError, TwoFactorActivation, TwoFactorChallenge,
TwoFactorPendingIdentity, TwoFactorSetup, TwoFactorStatus, VerifiedMachineCredential,
};
pub use ext::billing::{
BillingError, BillingGate, BillingHook, NoopBillingHook, SharedBillingHook,
LoginPayload, MachineCredentialVerifier, MachineCredentialVerifierError,
SharedIdentityProvider, SharedMachineCredentialVerifier, VerifiedMachineCredential,
};
pub use ext::capability::{CapabilityProfile, CommunityCapabilityProfile};
pub use ext::metering::{MeteringEvent, MeteringSink, NoopMeteringSink, SharedMeteringSink};
@@ -54,13 +47,9 @@ pub use ext::protocol::{
ProtocolAdapter, ProtocolAdapterError, ResponseCacheScope, RuntimeRequestContext,
SharedProtocolAdapter,
};
pub use ext::tenancy::{
SharedTenantController, SingleTenantController, TenancyError, TenantController,
TenantResolutionContext,
};
pub use ids::{
AgentId, AuditEventId, AuthProfileId, DescriptorId, InvitationId, InvocationLogId, OperationId,
PlatformApiKeyId, SampleId, SecretId, TenantId, ToolId, UserId, UserSessionId, WorkspaceId,
PlatformApiKeyId, SampleId, SecretId, ToolId, UserId, UserSessionId, WorkspaceId,
};
pub use observability::{
InvocationLevel, InvocationLog, InvocationSource, InvocationStatus, UsagePeriod, UsageRollup,
-2
View File
@@ -56,8 +56,6 @@ fn empty_target_context(context: MappingTargetContext) -> Value {
("query".to_owned(), Value::Object(Map::new())),
("headers".to_owned(), Value::Object(Map::new())),
("body".to_owned(), Value::Object(Map::new())),
("variables".to_owned(), Value::Object(Map::new())),
("grpc".to_owned(), Value::Object(Map::new())),
])),
)])),
MappingTargetContext::Output => Value::Object(Map::from_iter([(
+1 -13
View File
@@ -9,11 +9,8 @@ pub enum JsonPathRoot {
RequestQuery,
RequestHeaders,
RequestBody,
RequestVariables,
RequestGrpc,
ResponseBody,
ResponseData,
ResponseGrpc,
Output,
}
@@ -25,11 +22,8 @@ impl JsonPathRoot {
Self::RequestQuery => "request.query",
Self::RequestHeaders => "request.headers",
Self::RequestBody => "request.body",
Self::RequestVariables => "request.variables",
Self::RequestGrpc => "request.grpc",
Self::ResponseBody => "response.body",
Self::ResponseData => "response.data",
Self::ResponseGrpc => "response.grpc",
Self::Output => "output",
}
}
@@ -41,11 +35,8 @@ impl JsonPathRoot {
Self::RequestQuery => &["request", "query"],
Self::RequestHeaders => &["request", "headers"],
Self::RequestBody => &["request", "body"],
Self::RequestVariables => &["request", "variables"],
Self::RequestGrpc => &["request", "grpc"],
Self::ResponseBody => &["response", "body"],
Self::ResponseData => &["response", "data"],
Self::ResponseGrpc => &["response", "grpc"],
Self::Output => &["output"],
}
}
@@ -125,16 +116,13 @@ impl JsonPath {
}
fn match_root(input: &str) -> Option<(JsonPathRoot, &str)> {
const ROOTS: [(&str, JsonPathRoot); 11] = [
("request.variables", JsonPathRoot::RequestVariables),
const ROOTS: [(&str, JsonPathRoot); 8] = [
("request.headers", JsonPathRoot::RequestHeaders),
("response.body", JsonPathRoot::ResponseBody),
("response.data", JsonPathRoot::ResponseData),
("response.grpc", JsonPathRoot::ResponseGrpc),
("request.path", JsonPathRoot::RequestPath),
("request.query", JsonPathRoot::RequestQuery),
("request.body", JsonPathRoot::RequestBody),
("request.grpc", JsonPathRoot::RequestGrpc),
("output", JsonPathRoot::Output),
("mcp", JsonPathRoot::Mcp),
];
+2 -8
View File
@@ -118,9 +118,7 @@ fn target_root_context(root: JsonPathRoot) -> Option<MappingTargetContext> {
JsonPathRoot::RequestPath
| JsonPathRoot::RequestQuery
| JsonPathRoot::RequestHeaders
| JsonPathRoot::RequestBody
| JsonPathRoot::RequestVariables
| JsonPathRoot::RequestGrpc => Some(MappingTargetContext::Input),
| JsonPathRoot::RequestBody => Some(MappingTargetContext::Input),
JsonPathRoot::Output => Some(MappingTargetContext::Output),
_ => None,
}
@@ -136,9 +134,7 @@ fn validate_source_root(
MappingTargetContext::Output => {
matches!(
root,
JsonPathRoot::ResponseBody
| JsonPathRoot::ResponseData
| JsonPathRoot::ResponseGrpc
JsonPathRoot::ResponseBody | JsonPathRoot::ResponseData
)
}
MappingTargetContext::Mixed => false,
@@ -166,8 +162,6 @@ fn validate_target_root(
| JsonPathRoot::RequestQuery
| JsonPathRoot::RequestHeaders
| JsonPathRoot::RequestBody
| JsonPathRoot::RequestVariables
| JsonPathRoot::RequestGrpc
),
MappingTargetContext::Output => root == JsonPathRoot::Output,
MappingTargetContext::Mixed => false,
-16
View File
@@ -25,22 +25,6 @@ pub enum RegistryError {
PlatformApiKeyNotFound { key_id: String },
#[error("secret {secret_id} was not found")]
SecretNotFound { secret_id: String },
#[error("stream session {session_id} was not found")]
StreamSessionNotFound { session_id: String },
#[error("async job {job_id} was not found")]
AsyncJobNotFound { job_id: String },
#[error("invalid stream session transition for {session_id}: {from} -> {to}")]
InvalidStreamSessionTransition {
session_id: String,
from: String,
to: String,
},
#[error("invalid async job transition for {job_id}: {from} -> {to}")]
InvalidAsyncJobTransition {
job_id: String,
from: String,
to: String,
},
#[error("secret with name {name} already exists in workspace {workspace_id}")]
SecretNameAlreadyExists { workspace_id: String, name: String },
#[error("secret {secret_id} is referenced by auth profile {auth_profile_id}")]
+1 -1
View File
@@ -584,7 +584,7 @@ fn join_target_url(base: &str, path: &str) -> String {
return base.to_owned();
}
if path.starts_with("http://") || path.starts_with("https://") || path.starts_with("grpc://") {
if path.starts_with("http://") || path.starts_with("https://") {
return path.to_owned();
}
+4 -4
View File
@@ -706,18 +706,18 @@ mod tests {
};
store
.put_bucket("tenant:alpha", bucket, Duration::from_secs(30))
.put_bucket("workspace:alpha", bucket, Duration::from_secs(30))
.await
.unwrap();
assert_eq!(
store.get_bucket("tenant:alpha").await.unwrap(),
store.get_bucket("workspace:alpha").await.unwrap(),
Some(bucket)
);
store.delete_bucket("tenant:alpha").await.unwrap();
store.delete_bucket("workspace:alpha").await.unwrap();
assert_eq!(store.get_bucket("tenant:alpha").await.unwrap(), None);
assert_eq!(store.get_bucket("workspace:alpha").await.unwrap(), None);
}
#[tokio::test]
File diff suppressed because it is too large Load Diff
-1
View File
@@ -22,7 +22,6 @@ Base path:
## Capabilities
- `GET /api/admin/capabilities`
- `GET /api/admin/workspaces/{workspace_id}/protocol-capabilities`
Community capabilities:
-2
View File
@@ -6,8 +6,6 @@
- `REST`
Коммерческие протоколы и их smoke targets живут в private репозиториях и не входят в Community release baseline.
Все примеры ниже дублируются готовыми payload-файлами в [examples/mcp-smoke](../examples/mcp-smoke).
## 1. Источник