From c77065756d16311e8303e2b237127a565fae129d Mon Sep 17 00:00:00 2001 From: github-ops Date: Sun, 21 Jun 2026 11:20:12 +0000 Subject: [PATCH] Refresh community demo and docs --- .env.example | 2 +- README.md | 9 +- apps/admin-api/src/service/demo.rs | 262 ++++++------ apps/admin-api/src/service/upstreams.rs | 24 +- .../integration/community_access_usage.rs | 19 +- apps/ui/js/i18n.js | 20 +- apps/ui/tests/e2e/operations.spec.js | 6 +- crates/crank-registry/src/migrations.rs | 8 +- deploy/community/.env.example | 2 +- deploy/community/.env.images.example | 2 +- docs/README.md | 29 ++ docs/deployment.md | 86 ++-- docs/installation.md | 77 ++++ docs/intro.md | 50 +++ docs/manual-regression-checklist.md | 2 +- docs/observability.md | 34 ++ docs/public-smoke-targets.md | 97 +---- docs/quickstart.md | 81 ++++ docs/runtime-config.md | 379 +++++------------- docs/secrets-and-auth.md | 33 ++ docs/ui.md | 54 +++ .../mcp-smoke/rest-open-meteo.operation.json | 120 ------ .../mcp-smoke/rest-open-meteo.operation.yaml | 95 ----- .../mcp-smoke/rest-open-meteo.test-input.json | 4 - 24 files changed, 688 insertions(+), 807 deletions(-) create mode 100644 docs/README.md create mode 100644 docs/installation.md create mode 100644 docs/intro.md create mode 100644 docs/observability.md create mode 100644 docs/quickstart.md create mode 100644 docs/secrets-and-auth.md create mode 100644 docs/ui.md delete mode 100644 examples/mcp-smoke/rest-open-meteo.operation.json delete mode 100644 examples/mcp-smoke/rest-open-meteo.operation.yaml delete mode 100644 examples/mcp-smoke/rest-open-meteo.test-input.json diff --git a/.env.example b/.env.example index afe4348..eac43b9 100644 --- a/.env.example +++ b/.env.example @@ -32,5 +32,5 @@ CRANK_SESSION_TTL_HOURS=24 CRANK_BOOTSTRAP_ADMIN_EMAIL=owner@crank.local CRANK_BOOTSTRAP_ADMIN_PASSWORD=change-me-admin-password CRANK_BOOTSTRAP_ADMIN_DISPLAY_NAME=Crank Owner -CRANK_DEMO_SEED=false +CRANK_DEMO_SEED=true CRANK_BASE_URL=https://crank.example.com diff --git a/README.md b/README.md index d8325a9..4bd962d 100644 --- a/README.md +++ b/README.md @@ -257,9 +257,14 @@ npx playwright test ## Документация -- [Настройки запуска](docs/runtime-config.md) -- [Развертывание](docs/deployment.md) +- [Документация](docs/README.md) +- [Введение](docs/intro.md) +- [Установка](docs/installation.md) +- [Первый инструмент](docs/quickstart.md) +- [Веб-интерфейс](docs/ui.md) - [MCP-интерфейс](docs/mcp-interface.md) +- [Admin API](docs/admin-api.md) +- [Настройки запуска](docs/runtime-config.md) - [Английский README](docs/en/README.md) ## Участие в разработке diff --git a/apps/admin-api/src/service/demo.rs b/apps/admin-api/src/service/demo.rs index e07487f..6042c88 100644 --- a/apps/admin-api/src/service/demo.rs +++ b/apps/admin-api/src/service/demo.rs @@ -2,10 +2,11 @@ use std::collections::BTreeMap; use crank_core::{ AgentId, InvocationLevel, InvocationSource, InvocationStatus, MembershipRole, OperationId, - OperationSecurityLevel, OperationStatus, PlatformApiKeyScope, PlatformApiKeyStatus, Protocol, - Target, WizardState, WorkspaceId, + OperationSecurityLevel, PlatformApiKeyScope, PlatformApiKeyStatus, Protocol, Target, + WizardState, WorkspaceId, }; use crank_mapping::{JsonPathRoot, infer_mapping_from_samples}; +use crank_mapping::{MappingRule, MappingSet}; use crank_registry::{ListInvocationLogsQuery, OperationSummary, SampleKind}; use crank_schema::Schema; use serde_json::{Value, json}; @@ -28,6 +29,8 @@ impl AdminService { .ensure_membership(workspace_id, owner_user_id, MembershipRole::Owner) .await?; + self.cleanup_legacy_demo_assets(workspace_id).await?; + let rest_operation = self .ensure_demo_operation(workspace_id, demo_rest_operation_payload()) .await?; @@ -41,25 +44,20 @@ impl AdminService { ) .await?; - let archived_operation = self - .ensure_demo_operation(workspace_id, demo_archived_operation_payload()) - .await?; - self.ensure_operation_archived(workspace_id, &archived_operation) - .await?; - - let revops_agent = self - .ensure_demo_agent(workspace_id, demo_revops_agent_payload()) + let currency_agent = self + .ensure_demo_agent(workspace_id, demo_currency_agent_payload()) .await?; self.ensure_demo_agent_bindings( workspace_id, - &AgentId::new(revops_agent.id.clone()), + &AgentId::new(currency_agent.id.clone()), vec![AgentBindingPayload { operation_id: rest_operation.id.as_str().to_owned(), operation_version: rest_operation.current_draft_version, - tool_name: "create_crm_lead".to_owned(), - tool_title: "Create CRM Lead".to_owned(), + tool_name: "frankfurter_latest_rate".to_owned(), + tool_title: "Последний курс валюты".to_owned(), tool_description_override: Some( - "Create a new CRM lead in the revenue workspace.".to_owned(), + "Возвращает последний доступный курс одной валюты к другой через Frankfurter." + .to_owned(), ), enabled: true, }], @@ -69,8 +67,8 @@ impl AdminService { self.ensure_demo_platform_api_key( workspace_id, - &AgentId::new(revops_agent.id.clone()), - "Web Console Demo Key", + &AgentId::new(currency_agent.id.clone()), + "Frankfurter Demo Key", vec![PlatformApiKeyScope::Read, PlatformApiKeyScope::Write], false, ) @@ -78,7 +76,7 @@ impl AdminService { self.seed_demo_invocation_logs( workspace_id, - &AgentId::new(revops_agent.id), + &AgentId::new(currency_agent.id), &rest_operation.id, ) .await?; @@ -86,6 +84,46 @@ impl AdminService { Ok(()) } + async fn cleanup_legacy_demo_assets(&self, workspace_id: &WorkspaceId) -> Result<(), ApiError> { + for slug in ["revops-copilot", "support-triage"] { + if let Some(agent) = self.find_agent_by_slug(workspace_id, slug).await? { + self.delete_agent(workspace_id, &AgentId::new(agent.id.as_str().to_owned())) + .await?; + } + } + + let operations = self.registry.list_operations(workspace_id).await?; + for operation in operations { + if operation.name.starts_with("internal_health_smoke_") + || operation + .name + .starts_with("weather_current_open_meteo_smoke_") + { + self.delete_operation( + workspace_id, + &OperationId::new(operation.id.as_str().to_owned()), + ) + .await?; + } + } + + for name in [ + "crm_create_lead", + "marketing_archive_contact", + "weather_current_open_meteo", + ] { + if let Some(operation) = self.find_operation_by_name(workspace_id, name).await? { + self.delete_operation( + workspace_id, + &OperationId::new(operation.id.as_str().to_owned()), + ) + .await?; + } + } + + Ok(()) + } + async fn ensure_demo_platform_api_key( &self, workspace_id: &WorkspaceId, @@ -157,19 +195,6 @@ impl AdminService { Ok(()) } - async fn ensure_operation_archived( - &self, - workspace_id: &WorkspaceId, - summary: &OperationSummary, - ) -> Result<(), ApiError> { - if summary.status == OperationStatus::Archived { - return Ok(()); - } - - self.archive_operation(workspace_id, &summary.id).await?; - Ok(()) - } - async fn ensure_demo_json_samples( &self, workspace_id: &WorkspaceId, @@ -241,7 +266,7 @@ impl AdminService { async fn seed_demo_invocation_logs( &self, workspace_id: &WorkspaceId, - revops_agent_id: &AgentId, + currency_agent_id: &AgentId, rest_operation_id: &OperationId, ) -> Result<(), ApiError> { if !self @@ -273,21 +298,21 @@ impl AdminService { .await?; self.record_invocation(InvocationRecordRequest { workspace_id, - agent_id: Some(revops_agent_id), + agent_id: Some(currency_agent_id), operation: &rest_operation.snapshot, request_id: None, source: InvocationSource::AgentToolCall, level: InvocationLevel::Info, status: InvocationStatus::Ok, - message: "lead created in CRM".to_owned(), - status_code: Some(201), + message: "Frankfurter returned latest exchange rate".to_owned(), + status_code: Some(200), error_kind: None, - duration_ms: 182, + duration_ms: 124, request_preview: json!({ "path": {}, - "query": {}, - "headers": { "x-demo-source": "crank-seed" }, - "body": demo_rest_request_sample() + "query": demo_rest_request_sample(), + "headers": { "Accept": "application/json" }, + "body": null }), response_preview: demo_rest_response_sample(), }) @@ -296,47 +321,41 @@ impl AdminService { } } -fn demo_revops_agent_payload() -> AgentPayload { +fn demo_currency_agent_payload() -> AgentPayload { AgentPayload { - slug: "revops-copilot".to_owned(), - display_name: "RevOps Copilot".to_owned(), - description: "Sales operations assistant with CRM tools.".to_owned(), + slug: "currency-rates".to_owned(), + display_name: "Курсы валют".to_owned(), + description: "Агент с инструментами для получения курсов валют.".to_owned(), instructions: json!({ - "system": "Prefer CRM mutations first, then lookup tools for confirmation." + "system": "Используй инструменты Frankfurter только для запросов о курсах валют." }), tool_selection_policy: json!({ - "max_tools": 8, - "prefer_tag": ["sales", "finance"] + "max_tools": 4, + "prefer_tag": ["currency", "exchange-rate"] }), } } fn demo_rest_operation_payload() -> OperationPayload { let input = demo_rest_input_sample(); - let request = demo_rest_request_sample(); let response = demo_rest_response_sample(); let output = demo_rest_output_sample(); OperationPayload { - name: "crm_create_lead".to_owned(), - display_name: "Create CRM Lead".to_owned(), - category: "sales".to_owned(), + name: "frankfurter_latest_rate".to_owned(), + display_name: "Последний курс валюты".to_owned(), + category: "frankfurter_rates".to_owned(), protocol: Protocol::Rest, security_level: OperationSecurityLevel::Standard, target: Target::Rest(crank_core::RestTarget { - base_url: "https://crm.demo.internal".to_owned(), - method: crank_core::HttpMethod::Post, - path_template: "/v1/leads".to_owned(), - static_headers: BTreeMap::from([("x-demo-source".to_owned(), "crank-seed".to_owned())]), + base_url: "https://api.frankfurter.dev".to_owned(), + method: crank_core::HttpMethod::Get, + path_template: "/v1/latest".to_owned(), + static_headers: BTreeMap::from([("Accept".to_owned(), "application/json".to_owned())]), }), input_schema: Schema::from_json_sample(&input), output_schema: Schema::from_json_sample(&output), - input_mapping: infer_mapping_from_samples( - &input, - JsonPathRoot::Mcp, - &request, - JsonPathRoot::RequestBody, - ), + input_mapping: frankfurter_input_mapping(), output_mapping: infer_mapping_from_samples( &response, JsonPathRoot::ResponseBody, @@ -353,13 +372,19 @@ fn demo_rest_operation_payload() -> OperationPayload { headers: BTreeMap::new(), }, tool_description: crank_core::ToolDescription { - title: "Create CRM Lead".to_owned(), - description: "Create a lead record in the CRM system.".to_owned(), - tags: vec!["sales".to_owned(), "crm".to_owned()], + title: "Последний курс валюты".to_owned(), + description: + "Возвращает последний доступный курс одной валюты к другой через Frankfurter." + .to_owned(), + tags: vec![ + "frankfurter".to_owned(), + "currency".to_owned(), + "exchange-rate".to_owned(), + ], examples: vec![crank_core::ToolExample { input: json!({ - "email": "sarah.connor@example.com", - "company": "Cyberdyne" + "base": "USD", + "quote": "EUR" }), }], }, @@ -371,91 +396,56 @@ fn demo_rest_operation_payload() -> OperationPayload { } } -fn demo_archived_operation_payload() -> OperationPayload { - let input = json!({ - "contactId": "contact_123", - "reason": "Duplicate profile" - }); - let request = json!({ - "contactId": "contact_123", - "reason": "Duplicate profile" - }); - let response = json!({ - "archived": true, - "contactId": "contact_123" - }); - - OperationPayload { - name: "marketing_archive_contact".to_owned(), - display_name: "Archive Marketing Contact".to_owned(), - category: "marketing".to_owned(), - protocol: Protocol::Rest, - security_level: OperationSecurityLevel::Standard, - target: Target::Rest(crank_core::RestTarget { - base_url: "https://marketing.demo.internal".to_owned(), - method: crank_core::HttpMethod::Patch, - path_template: "/v1/contacts/archive".to_owned(), - static_headers: BTreeMap::new(), - }), - input_schema: Schema::from_json_sample(&input), - output_schema: Schema::from_json_sample(&response), - input_mapping: infer_mapping_from_samples( - &input, - JsonPathRoot::Mcp, - &request, - JsonPathRoot::RequestBody, - ), - output_mapping: infer_mapping_from_samples( - &response, - JsonPathRoot::ResponseBody, - &response, - JsonPathRoot::Output, - ), - execution_config: crank_core::ExecutionConfig { - timeout_ms: 6_000, - retry_policy: None, - response_cache: None, - idempotency: None, - safety: None, - auth_profile_ref: None, - headers: BTreeMap::new(), - }, - tool_description: crank_core::ToolDescription { - title: "Archive Marketing Contact".to_owned(), - description: "Legacy archived flow kept for audit only.".to_owned(), - tags: vec!["marketing".to_owned()], - examples: Vec::new(), - }, - wizard_state: Some(WizardState { - input_sample: Some(input), - output_sample: Some(response), - test_input: Some(request), - }), - } -} - fn demo_rest_input_sample() -> Value { json!({ - "firstName": "Sarah", - "lastName": "Connor", - "email": "sarah.connor@example.com", - "company": "Cyberdyne", - "source": "website" + "base": "USD", + "quote": "EUR" }) } fn demo_rest_request_sample() -> Value { - demo_rest_input_sample() + json!({ + "base": "USD", + "symbols": "EUR" + }) } fn demo_rest_response_sample() -> Value { json!({ - "id": "lead_1001", - "status": "created", - "owner": "revops" + "amount": 1.0, + "base": "USD", + "date": "2026-06-19", + "rates": { + "EUR": 0.87207 + } }) } fn demo_rest_output_sample() -> Value { demo_rest_response_sample() } + +fn frankfurter_input_mapping() -> MappingSet { + MappingSet { + rules: vec![ + MappingRule { + source: "$.mcp.base".to_owned(), + target: "$.request.query.base".to_owned(), + required: true, + default_value: None, + transform: None, + condition: None, + notes: None, + }, + MappingRule { + source: "$.mcp.quote".to_owned(), + target: "$.request.query.symbols".to_owned(), + required: true, + default_value: None, + transform: None, + condition: None, + notes: None, + }, + ], + } +} diff --git a/apps/admin-api/src/service/upstreams.rs b/apps/admin-api/src/service/upstreams.rs index 799ef09..4cbf7be 100644 --- a/apps/admin-api/src/service/upstreams.rs +++ b/apps/admin-api/src/service/upstreams.rs @@ -85,19 +85,33 @@ impl AdminService { workspace_id: &WorkspaceId, ) -> Result<(), ApiError> { let existing = self.registry.list_workspace_upstreams(workspace_id).await?; - if existing.iter().any(|item| item.name == "Open Meteo") { + if existing.iter().any(|item| item.name == "Frankfurter") { return Ok(()); } let now = OffsetDateTime::now_utc(); + let existing_open_meteo = existing + .iter() + .find(|item| { + item.name == "Open Meteo" + && item.base_url == "https://api.open-meteo.com" + && item.auth_profile_id.is_none() + }) + .cloned(); let upstream = WorkspaceUpstream { - id: WorkspaceUpstreamId::new(new_prefixed_id("upstream")), + id: existing_open_meteo + .as_ref() + .map(|item| item.id.clone()) + .unwrap_or_else(|| WorkspaceUpstreamId::new(new_prefixed_id("upstream"))), workspace_id: workspace_id.clone(), - name: "Open Meteo".to_owned(), - base_url: "https://api.open-meteo.com".to_owned(), + name: "Frankfurter".to_owned(), + base_url: "https://api.frankfurter.dev".to_owned(), static_headers: json!({}), auth_profile_id: None, - created_at: now, + created_at: existing_open_meteo + .as_ref() + .map(|item| item.created_at) + .unwrap_or(now), updated_at: now, }; self.registry diff --git a/apps/admin-api/tests/integration/community_access_usage.rs b/apps/admin-api/tests/integration/community_access_usage.rs index dc5ef39..69b3c7e 100644 --- a/apps/admin-api/tests/integration/community_access_usage.rs +++ b/apps/admin-api/tests/integration/community_access_usage.rs @@ -421,10 +421,25 @@ async fn seeds_demo_assets_for_live_ui() { .list_operations(&default_workspace_id) .await .unwrap(); - assert!(operations.len() >= 2); + assert!( + operations + .iter() + .any(|operation| operation.name == "frankfurter_latest_rate") + ); + assert!( + !operations + .iter() + .any(|operation| operation.name == "crm_create_lead") + ); + assert!( + !operations + .iter() + .any(|operation| operation.name.starts_with("internal_health_smoke_")) + ); let agents = service.list_agents(&default_workspace_id).await.unwrap(); - assert!(!agents.is_empty()); + assert_eq!(agents.len(), 1); + assert_eq!(agents[0].slug, "currency-rates"); assert!(agents.iter().any(|agent| agent.key_count > 0)); diff --git a/apps/ui/js/i18n.js b/apps/ui/js/i18n.js index 8747dea..6090436 100644 --- a/apps/ui/js/i18n.js +++ b/apps/ui/js/i18n.js @@ -796,13 +796,9 @@ var TRANSLATIONS = { 'agents.toast.endpoint_title': 'MCP endpoint copied', // Demo content - 'demo.agent.revops-copilot.display_name': 'RevOps Copilot', - 'demo.agent.support-triage.display_name': 'Support Triage', - '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.support_lookup_ticket.display_name': 'Lookup Support Ticket', - 'demo.operation.marketing_archive_contact.display_name': 'Archive Marketing Contact', - 'demo.operation.marketing_archive_contact.description': 'Legacy archived flow kept for audit only.', + 'demo.agent.currency-rates.display_name': 'Currency rates', + 'demo.operation.frankfurter_latest_rate.display_name': 'Latest currency rate', + 'demo.operation.frankfurter_latest_rate.description': 'Returns the latest available exchange rate through Frankfurter.', // Login 'login.title': 'Sign in', @@ -1612,13 +1608,9 @@ var TRANSLATIONS = { 'agents.toast.endpoint_title': 'MCP endpoint скопирован', // Demo content - 'demo.agent.revops-copilot.display_name': 'Помощник RevOps', - 'demo.agent.support-triage.display_name': 'Триаж поддержки', - 'demo.operation.crm_create_lead.display_name': 'Создать CRM-лид', - 'demo.operation.crm_create_lead.description': 'Создает запись лида в CRM-системе.', - 'demo.operation.support_lookup_ticket.display_name': 'Найти тикет поддержки', - 'demo.operation.marketing_archive_contact.display_name': 'Архивировать маркетинговый контакт', - 'demo.operation.marketing_archive_contact.description': 'Устаревший архивный сценарий, оставленный только для аудита.', + 'demo.agent.currency-rates.display_name': 'Курсы валют', + 'demo.operation.frankfurter_latest_rate.display_name': 'Последний курс валюты', + 'demo.operation.frankfurter_latest_rate.description': 'Возвращает последний доступный курс валюты через Frankfurter.', // Login 'login.title': 'Войти', diff --git a/apps/ui/tests/e2e/operations.spec.js b/apps/ui/tests/e2e/operations.spec.js index 154df2e..628ddd5 100644 --- a/apps/ui/tests/e2e/operations.spec.js +++ b/apps/ui/tests/e2e/operations.spec.js @@ -6,8 +6,8 @@ test('operations page shows demo catalog and filter works', async ({ page }) => await expect(page.locator('.page-heading')).toHaveText(localized('Operations', 'Операции')); await expect(page.locator('.ws-switcher-trigger')).toBeVisible(); await expect(page.locator('#ws-dropdown')).toHaveCount(0); - await expect(page.locator('tbody tr')).toHaveCount(2); - await page.getByPlaceholder(localized('Search operations', 'Поиск операций')).fill('crm'); await expect(page.locator('tbody tr')).toHaveCount(1); - await expect(page.locator('tbody tr').first()).toContainText(/crm_create_lead/i); + await page.getByPlaceholder(localized('Search operations', 'Поиск операций')).fill('frankfurter'); + await expect(page.locator('tbody tr')).toHaveCount(1); + await expect(page.locator('tbody tr').first()).toContainText(/frankfurter_latest_rate/i); }); diff --git a/crates/crank-registry/src/migrations.rs b/crates/crank-registry/src/migrations.rs index dd77dc4..b7acea6 100644 --- a/crates/crank-registry/src/migrations.rs +++ b/crates/crank-registry/src/migrations.rs @@ -379,10 +379,10 @@ pub async fn apply_postgres(pool: &PgPool) -> Result<(), sqlx::Error> { updated_at ) select - 'upstream_open_meteo_' || w.id, + 'upstream_frankfurter_' || w.id, w.id, - 'Open Meteo', - 'https://api.open-meteo.com', + 'Frankfurter', + 'https://api.frankfurter.dev', '{}'::jsonb, null, now(), @@ -392,7 +392,7 @@ pub async fn apply_postgres(pool: &PgPool) -> Result<(), sqlx::Error> { select 1 from workspace_upstreams wu where wu.workspace_id = w.id - and wu.name = 'Open Meteo' + and wu.name = 'Frankfurter' )", ) .execute(pool) diff --git a/deploy/community/.env.example b/deploy/community/.env.example index 351b034..8a09e4a 100644 --- a/deploy/community/.env.example +++ b/deploy/community/.env.example @@ -35,5 +35,5 @@ CRANK_SESSION_TTL_HOURS=24 CRANK_BOOTSTRAP_ADMIN_EMAIL=owner@crank.local CRANK_BOOTSTRAP_ADMIN_PASSWORD=change-me-admin-password CRANK_BOOTSTRAP_ADMIN_DISPLAY_NAME=Crank Owner -CRANK_DEMO_SEED=false +CRANK_DEMO_SEED=true CRANK_BASE_URL=https://crank.example.com diff --git a/deploy/community/.env.images.example b/deploy/community/.env.images.example index f65c405..bb75d21 100644 --- a/deploy/community/.env.images.example +++ b/deploy/community/.env.images.example @@ -29,5 +29,5 @@ CRANK_SESSION_TTL_HOURS=24 CRANK_BOOTSTRAP_ADMIN_EMAIL=owner@crank.local CRANK_BOOTSTRAP_ADMIN_PASSWORD=change-me-admin-password CRANK_BOOTSTRAP_ADMIN_DISPLAY_NAME=Crank Owner -CRANK_DEMO_SEED=false +CRANK_DEMO_SEED=true CRANK_BASE_URL=https://crank.example.com diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..35d0220 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,29 @@ +# Документация Crank + +Crank превращает REST API endpoint-ы в MCP-инструменты, которые можно подключать к AI-агентам и MCP-клиентам. + +Эта документация подготовлена как основа для будущего сайта. Сейчас файлы лежат в `docs/`, позже их можно перенести в Docusaurus без изменения структуры тем. + +## Начать + +- [Введение](./intro.md) +- [Установка](./installation.md) +- [Первый инструмент](./quickstart.md) +- [Подключение MCP-клиента](./mcp-interface.md) + +## Возможности + +- [Веб-интерфейс](./ui.md) +- [REST-инструменты](./protocols/rest.md) +- [Секреты и профили авторизации](./secrets-and-auth.md) +- [Журналы и использование](./observability.md) + +## Справочник + +- [Настройки окружения](./runtime-config.md) +- [Admin API](./admin-api.md) +- [Развертывание](./deployment.md) +- [Архитектура](./architecture.md) +- [Модель данных](./data-model.md) +- [Тестирование](./testing-strategy.md) + diff --git a/docs/deployment.md b/docs/deployment.md index a7a21f9..528678a 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -1,17 +1,16 @@ -# Deployment +# Развертывание -Документ описывает поддерживаемый путь деплоя Crank Community. +Документ описывает поддерживаемый путь запуска Crank на сервере. -Crank Community запускается как три application containers за reverse proxy: +Crank запускается как три контейнера за reverse proxy: - `ui` - `admin-api` - `mcp-server` -Приложение использует внешний PostgreSQL. Compose manifest не поднимает -PostgreSQL самостоятельно. +Можно использовать внешний PostgreSQL или локальный PostgreSQL из compose-профиля `local-db`. -## Runtime topology +## Схема ```text reverse proxy @@ -21,28 +20,27 @@ reverse proxy admin-api -> PostgreSQL mcp-server -> PostgreSQL -admin-api -> optional Valkey/Redis -mcp-server -> optional Valkey/Redis +admin-api -> Valkey/Redis, опционально +mcp-server -> Valkey/Redis, опционально ``` -## Deployment files +## Файлы запуска - `deploy/community/docker-compose.yml` - `deploy/community/.env.example` -- `.gitea/workflows/ci.yml` -- `.gitea/workflows/deploy.yml` -- `.gitea/workflows/release.yml` +- `deploy/community/docker-compose.images.yml` +- `deploy/community/.env.images.example` ## Порты -Default service ports: +Порты по умолчанию: - `ui`: `3000` - `admin-api`: `3001` - `mcp-server`: `3002` - optional `valkey`: `6379`, только loopback -`CRANK_PUBLISH_BIND` управляет публикацией application ports: +`CRANK_PUBLISH_BIND` управляет публикацией портов: - `127.0.0.1`, если reverse proxy работает на том же host; - `0.0.0.0`, если reverse proxy работает на другом host. @@ -94,9 +92,9 @@ server { Замените `192.168.1.106` на адрес deployment host. -## Compose +## Запуск из исходников -Проверка manifest: +Проверка compose-файла: ```bash docker compose \ @@ -105,7 +103,7 @@ docker compose \ config -q ``` -Запуск без внешнего cache: +Запуск с внешним PostgreSQL: ```bash docker compose \ @@ -132,7 +130,24 @@ CRANK_CACHE_URL=redis://valkey:6379/0 CRANK_CACHE_DEFAULT_TTL_MS=60000 ``` -## Health checks +## Запуск готовых образов + +```bash +mkdir -p crank +cd crank +curl -fsSLo docker-compose.yml https://git.itexp.me/bsodfather/crank/raw/branch/main/deploy/community/docker-compose.images.yml +curl -fsSLo .env.example https://git.itexp.me/bsodfather/crank/raw/branch/main/deploy/community/.env.images.example +cp .env.example .env +docker compose --profile local-db up -d +``` + +Если используется внешний PostgreSQL, заполните `POSTGRES_HOST`, `POSTGRES_PORT`, `POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD` и запустите: + +```bash +docker compose up -d +``` + +## Проверка состояния ```bash curl http://127.0.0.1:3001/health @@ -146,38 +161,15 @@ curl http://127.0.0.1:3002/health {"service":"mcp-server","status":"ok"} ``` -UI root должен возвращать `200 OK`: +UI должен возвращать `200 OK`: ```bash curl -I http://127.0.0.1:3000/ ``` -## Gitea CI/CD +## Эксплуатация -Репозиторий использует Gitea Actions: - -- `.gitea/workflows/ci.yml` запускает Rust, UI, E2E и deployment manifest checks. -- `.gitea/workflows/deploy.yml` собирает images и деплоит `main`. -- `.gitea/workflows/release.yml` собирает release artifacts для tags. - -Deploy workflow читает из Gitea secrets только OpenBao bootstrap credentials: - -- `BAO_ADDR` -- `BAO_ROLE_ID` -- `BAO_SECRET_ID` - -Дальше workflow читает KV v2 secrets из OpenBao: - -```text -ci/shared/registry -ci/shared/deploy-ssh -ci/projects/crank/deploy -ci/projects/crank/runtime -``` - -## Operational notes - -- Бэкапы БД должны жить вне application host. -- Runtime secrets хранятся в OpenBao, не в Git. -- Для rollback используйте immutable image tags. -- `CRANK_PUBLISH_BIND=0.0.0.0` нужен только если другой host должен обращаться к published ports напрямую. +- Делайте регулярные бэкапы PostgreSQL. +- Не храните реальные секреты в Git. +- Для rollback используйте конкретные image tags, а не только `main`. +- `CRANK_PUBLISH_BIND=0.0.0.0` нужен только если reverse proxy работает на другом host. diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..9078364 --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,77 @@ +# Установка + +Самый простой способ запустить Crank - использовать готовые Docker-образы и `docker compose`. + +## Требования + +- Docker; +- Docker Compose; +- PostgreSQL или локальный compose-профиль `local-db`; +- свободные порты `3000`, `3001`, `3002`. + +## Быстрый запуск + +```bash +mkdir -p crank +cd crank +curl -fsSLo docker-compose.yml https://git.itexp.me/bsodfather/crank/raw/branch/main/deploy/community/docker-compose.images.yml +curl -fsSLo .env.example https://git.itexp.me/bsodfather/crank/raw/branch/main/deploy/community/.env.images.example +cp .env.example .env +``` + +Откройте `.env` и замените значения: + +- `POSTGRES_PASSWORD`; +- `CRANK_MASTER_KEY`; +- `CRANK_SESSION_SECRET`; +- `CRANK_PASSWORD_PEPPER`; +- `CRANK_BOOTSTRAP_ADMIN_EMAIL`; +- `CRANK_BOOTSTRAP_ADMIN_PASSWORD`; +- `CRANK_BASE_URL`. + +Секреты можно сгенерировать командой: + +```bash +openssl rand -hex 32 +``` + +Запуск с локальным PostgreSQL из compose: + +```bash +docker compose --profile local-db up -d +``` + +Если PostgreSQL уже есть отдельно, укажите его в `.env` и запустите без профиля: + +```bash +docker compose up -d +``` + +## Проверка + +```bash +docker compose ps +curl http://127.0.0.1:3001/health +curl http://127.0.0.1:3002/health +``` + +После запуска откройте веб-интерфейс: + +```text +http://localhost:3000 +``` + +## Обновление + +```bash +docker compose --profile local-db pull +docker compose --profile local-db up -d +``` + +Если используете внешний PostgreSQL: + +```bash +docker compose pull +docker compose up -d +``` + diff --git a/docs/intro.md b/docs/intro.md new file mode 100644 index 0000000..3f959e1 --- /dev/null +++ b/docs/intro.md @@ -0,0 +1,50 @@ +# Что такое Crank + +Crank - это свободная платформа для создания MCP-инструментов из REST API endpoint-ов. + +Обычный REST API удобен для программ, но не всегда удобен для AI-агента. Агенту нужен понятный каталог инструментов: название, описание, входные параметры, ожидаемый результат и стабильный способ вызова. Crank берет существующий REST endpoint и описывает его как MCP-инструмент. + +## Что можно сделать + +- Описать REST endpoint через веб-интерфейс. +- Проверить запрос перед публикацией. +- Опубликовать инструмент в каталоге конкретного агента. +- Выдать API-ключ для MCP-клиента. +- Смотреть журнал вызовов и статистику использования. +- Хранить секреты для внешних API без отображения значения после сохранения. + +## Как это работает + +```text +MCP-клиент + -> Crank MCP server + -> опубликованный агент + -> выбранный MCP-инструмент + -> REST API +``` + +В Crank агент - это отдельный MCP endpoint со своим набором инструментов и своими API-ключами. MCP-клиент, подключенный к одному агенту, видит только инструменты этого агента. + +## Что входит в Community + +- один workspace; +- один пользователь администратора; +- любое количество агентов; +- REST-инструменты; +- MCP Streamable HTTP; +- статические API-ключи агентов; +- PostgreSQL как основное хранилище; +- опциональный Valkey или Redis для служебного кэша. + +## Демо при первом запуске + +В примерах окружения включен `CRANK_DEMO_SEED=true`. После первого запуска Crank создает: + +- upstream `Frankfurter`; +- операцию `frankfurter_latest_rate`; +- агента `currency-rates`; +- API-ключ агента; +- пример записи в журнале вызовов. + +Демо можно отключить, указав `CRANK_DEMO_SEED=false`. + diff --git a/docs/manual-regression-checklist.md b/docs/manual-regression-checklist.md index 9e6a654..94bd3b8 100644 --- a/docs/manual-regression-checklist.md +++ b/docs/manual-regression-checklist.md @@ -158,7 +158,7 @@ cd apps/ui && npm run e2e Обязательный кейс: -- REST: [rest-open-meteo.operation.json](../examples/mcp-smoke/rest-open-meteo.operation.json) +- REST: [Frankfurter examples](../examples/frankfurter/README.md) ## 7. Acceptance criteria diff --git a/docs/observability.md b/docs/observability.md new file mode 100644 index 0000000..22f4d31 --- /dev/null +++ b/docs/observability.md @@ -0,0 +1,34 @@ +# Журналы и использование + +Crank сохраняет данные о тестовых запусках и вызовах опубликованных MCP-инструментов. + +## Журналы + +В журнал попадают: + +- операция; +- агент, если вызов пришел через MCP; +- request id; +- статус; +- HTTP status code внешнего API; +- время выполнения; +- краткий preview запроса и ответа; +- категория ошибки, если вызов завершился ошибкой. + +## Использование + +Раздел использования агрегирует: + +- количество вызовов; +- успешные и ошибочные вызовы; +- долю ошибок; +- задержки p50, p95 и p99; +- распределение вызовов по операциям. + +## Для чего это нужно + +- проверить, вызывают ли агенты нужные инструменты; +- увидеть ошибки маппинга или внешнего API; +- найти медленные endpoint-ы; +- понять, какие инструменты реально используются. + diff --git a/docs/public-smoke-targets.md b/docs/public-smoke-targets.md index f99d4b3..fea7fee 100644 --- a/docs/public-smoke-targets.md +++ b/docs/public-smoke-targets.md @@ -1,99 +1,32 @@ -# Public Smoke Targets +# Публичный тестовый API -Этот документ фиксирует публичный upstream-сервис, который можно использовать для ручной проверки `REST` operation в `crank-community` без поднятия своего тестового backend-а. +Для демонстрации и ручных проверок Crank использует Frankfurter. -Для Community канонический smoke target только один: +Frankfurter - публичный API курсов валют без ключа доступа. -- `REST` - -Все примеры ниже дублируются готовыми payload-файлами в [examples/mcp-smoke](../examples/mcp-smoke). - -## 1. Источник - -- REST: Open-Meteo Weather Forecast API - `https://open-meteo.com/en/docs` - -## 2. Готовые operation payload-ы - -- REST: [rest-open-meteo.operation.json](../examples/mcp-smoke/rest-open-meteo.operation.json) -- REST test input: [rest-open-meteo.test-input.json](../examples/mcp-smoke/rest-open-meteo.test-input.json) - -## 3. Как использовать - -### 3.1. Через UI - -1. Создать operation вручную в `Wizard`. -2. Подставить значения из `rest-open-meteo.operation.json`. -3. На шаге теста использовать `rest-open-meteo.test-input.json`. - -### 3.2. Через admin-api - -Пример для `ws_default`: - -```bash -curl -sS -X POST \ - https://rmcp.itexp.me/api/admin/workspaces/ws_default/operations \ - -H 'content-type: application/json' \ - -b cookie.txt \ - --data @examples/mcp-smoke/rest-open-meteo.operation.json +```text +https://api.frankfurter.dev ``` -Потом test-run: +Рабочий пример: ```bash -curl -sS -X POST \ - https://rmcp.itexp.me/api/admin/workspaces/ws_default/operations//test-runs \ - -H 'content-type: application/json' \ - -b cookie.txt \ - --data '{ - "version": 1, - "input": '"$(cat examples/mcp-smoke/rest-open-meteo.test-input.json)"' - }' +curl 'https://api.frankfurter.dev/v1/latest?base=USD&symbols=EUR' ``` -Логин перед этим: - -```bash -curl -sS -c cookie.txt \ - -H 'content-type: application/json' \ - -X POST https://rmcp.itexp.me/api/auth/login \ - --data '{"email":"","password":""}' -``` - -## 4. Что именно проверяет пример - -### 4.1. REST: Open-Meteo - -- Protocol: `REST` -- Endpoint: `https://api.open-meteo.com/v1/forecast` -- Проверка: - - query mapping - - fixed query defaults - - JSON response extraction - -Ожидаемый upstream response shape: +Ожидаемый ответ: ```json { - "timezone": "Europe/Moscow", - "current": { - "time": "2026-04-05T22:30", - "temperature_2m": 3.4, - "wind_speed_10m": 8.3 + "amount": 1.0, + "base": "USD", + "date": "2026-06-19", + "rates": { + "EUR": 0.87207 } } ``` -## 5. Практическая рекомендация +Готовые YAML-примеры лежат в папке [`examples/frankfurter`](../examples/frankfurter/README.md). -Для первого smoke pass использовать operation: - -- `weather_current_open_meteo` - -Этого достаточно, чтобы проверить весь путь: - -- create operation -- test-run -- publish -- bind to agent -- MCP call через `workspace + agent` +Основной demo seed создает операцию `frankfurter_latest_rate` и агента `currency-rates`. diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100644 index 0000000..82d5e7d --- /dev/null +++ b/docs/quickstart.md @@ -0,0 +1,81 @@ +# Первый инструмент + +После установки в Crank уже есть демо-пример Frankfurter. Он показывает полный путь от REST endpoint-а до MCP-инструмента. + +## 1. Откройте операции + +Перейдите в раздел **Операции**. В списке должна быть операция: + +```text +frankfurter_latest_rate +``` + +Она вызывает публичный API: + +```text +GET https://api.frankfurter.dev/v1/latest +``` + +Входные параметры: + +```json +{ + "base": "USD", + "quote": "EUR" +} +``` + +Crank преобразует их в query-параметры: + +```text +base=USD&symbols=EUR +``` + +## 2. Проверьте операцию + +Откройте операцию и перейдите к тестированию. Используйте пример: + +```json +{ + "base": "USD", + "quote": "EUR" +} +``` + +Успешный ответ Frankfurter выглядит так: + +```json +{ + "amount": 1.0, + "base": "USD", + "date": "2026-06-19", + "rates": { + "EUR": 0.87207 + } +} +``` + +## 3. Откройте агента + +Перейдите в раздел **Агенты**. Демо создает агента: + +```text +currency-rates +``` + +Этот агент публикует только инструмент `frankfurter_latest_rate`. + +## 4. Создайте API-ключ + +Перейдите в раздел **API ключи**, выберите агента `currency-rates` и создайте ключ. Полное значение ключа показывается только один раз. + +## 5. Подключите MCP-клиент + +MCP endpoint агента имеет вид: + +```text +https://your-crank-host/mcp/v1/default/currency-rates +``` + +Клиент должен передавать API-ключ агента в заголовке авторизации. + diff --git a/docs/runtime-config.md b/docs/runtime-config.md index 5d6c803..e9fb680 100644 --- a/docs/runtime-config.md +++ b/docs/runtime-config.md @@ -1,313 +1,114 @@ -# Runtime Config +# Настройки окружения -## 1. Назначение документа +Crank настраивается через переменные окружения. Один и тот же набор переменных используется при запуске из исходников и при запуске готовых Docker-образов. -Этот документ фиксирует конфигурацию окружения, storage и базовые operational assumptions для MVP. +## PostgreSQL -Его задача - убрать неявные решения, которые обычно всплывают уже в процессе написания кода. - -## 2. Базовые решения для MVP - -- каноническая БД: `PostgreSQL` -- локальная разработка и тесты используют ту же `PostgreSQL`-модель хранения -- artifact storage: локальная файловая система -- MCP transport: `Streamable HTTP` -- admin API и mcp-server запускаются как отдельные приложения - -## 3. Artifact storage - -В MVP sample JSON, `.proto`, `descriptor set` и YAML import payload должны храниться в локальном файловом storage. - -Требования: - -- все файлы кладутся в контролируемый базовый каталог; -- в БД хранится только `storage_ref`; -- структура каталогов должна быть детерминированной; -- storage слой должен быть абстрагирован, чтобы потом заменить его на S3-compatible backend. - -Рекомендуемая структура: - -```text -var/crank/ - samples/ - descriptors/ - yaml-imports/ -``` - -## 4. Секреты и auth profiles - -Для целевой модели: - -- operation хранит только `auth_profile_ref`; -- `AuthProfile` хранит только ссылки на `secret_id`; -- plaintext секреты не должны попадать в YAML export; -- plaintext секреты не должны логироваться; -- runtime получает секрет только на короткое время перед upstream вызовом. - -Стартовая реализация: - -- `PostgreSQL`-backed secret store; -- `ciphertext` хранится в БД; -- шифрование выполняется через `CRANK_MASTER_KEY`; -- ключ шифрования приходит только из env. - -## 5. Переменные окружения - -Минимально ожидаются: +Обязательные параметры: - `POSTGRES_HOST` - `POSTGRES_PORT` - `POSTGRES_DB` - `POSTGRES_USER` - `POSTGRES_PASSWORD` -- `POSTGRES_MAX_CONNECTIONS` -- `POSTGRES_MIN_CONNECTIONS` -- `POSTGRES_ACQUIRE_TIMEOUT_MS` -- `POSTGRES_IDLE_TIMEOUT_MS` -- `POSTGRES_MAX_LIFETIME_MS` -- `CRANK_ADMIN_API_IMAGE` -- `CRANK_MCP_SERVER_IMAGE` -- `CRANK_UI_IMAGE` -- `CRANK_STORAGE_ROOT` -- `CRANK_PUBLISH_BIND` -- `CRANK_ADMIN_BIND` -- `CRANK_ADMIN_RATE_LIMIT_RPS` -- `CRANK_ADMIN_RATE_LIMIT_BURST` -- `CRANK_MCP_BIND` -- `CRANK_MCP_REFRESH_MS` -- `CRANK_MCP_RATE_LIMIT_RPS` -- `CRANK_MCP_RATE_LIMIT_BURST` + +Параметры пула соединений: + +- `POSTGRES_MAX_CONNECTIONS`, по умолчанию `20`; +- `POSTGRES_MIN_CONNECTIONS`, по умолчанию `2`; +- `POSTGRES_ACQUIRE_TIMEOUT_MS`, по умолчанию `5000`; +- `POSTGRES_IDLE_TIMEOUT_MS`, по умолчанию `600000`; +- `POSTGRES_MAX_LIFETIME_MS`, по умолчанию `1800000`. + +Если используется PgBouncer, укажите его адрес в `POSTGRES_HOST` и порт в `POSTGRES_PORT`. + +## HTTP-сервисы + +- `CRANK_ADMIN_BIND` - адрес `admin-api`, например `0.0.0.0:3001`. +- `CRANK_MCP_BIND` - адрес `mcp-server`, например `0.0.0.0:3002`. +- `CRANK_PUBLISH_BIND` - адрес публикации портов в Docker Compose. +- `CRANK_BASE_URL` - публичный URL веб-интерфейса. + +Для сервера за reverse proxy обычно подходит: + +```env +CRANK_PUBLISH_BIND=127.0.0.1 +``` + +Если reverse proxy работает на другом host: + +```env +CRANK_PUBLISH_BIND=0.0.0.0 +``` + +## Авторизация администратора + +- `CRANK_SESSION_SECRET` - ключ подписи браузерных сессий. +- `CRANK_PASSWORD_PEPPER` - дополнительный секрет для хэширования паролей. +- `CRANK_SESSION_TTL_HOURS` - срок жизни сессии в часах. +- `CRANK_BOOTSTRAP_ADMIN_EMAIL` - email первого пользователя. +- `CRANK_BOOTSTRAP_ADMIN_PASSWORD` - пароль первого пользователя. +- `CRANK_BOOTSTRAP_ADMIN_DISPLAY_NAME` - отображаемое имя первого пользователя. + +Первый пользователь создается или обновляется при старте `admin-api`. + +## Шифрование секретов + +- `CRANK_MASTER_KEY` - ключ шифрования сохраненных секретов. + +Этот ключ нужен `admin-api` и `mcp-server`. Если изменить ключ без миграции данных, ранее сохраненные секреты нельзя будет расшифровать. + +## Демо-данные + +- `CRANK_DEMO_SEED=true` создает пример Frankfurter при старте. +- `CRANK_DEMO_SEED=false` отключает demo seed. + +Demo seed идемпотентный: повторный старт не создает дубликаты. В стандартном примере создаются upstream `Frankfurter`, операция `frankfurter_latest_rate`, агент `currency-rates` и пример API-ключа. + +## MCP + +- `CRANK_MCP_REFRESH_MS` - как часто `mcp-server` обновляет опубликованный каталог инструментов. +- `CRANK_MCP_RATE_LIMIT_RPS` - лимит запросов в секунду. +- `CRANK_MCP_RATE_LIMIT_BURST` - допустимый короткий всплеск запросов. + +## Admin API + +- `CRANK_ADMIN_RATE_LIMIT_RPS` - лимит запросов в секунду. +- `CRANK_ADMIN_RATE_LIMIT_BURST` - допустимый короткий всплеск запросов. + +## Runtime limits + - `CRANK_RUNTIME_MAX_CONCURRENT_UNARY` - `CRANK_RUNTIME_MAX_CONCURRENT_WINDOW` - `CRANK_RUNTIME_MAX_CONCURRENT_SESSIONS` - `CRANK_RUNTIME_MAX_CONCURRENT_JOBS` -- `CRANK_LOG_LEVEL` -- `CRANK_MASTER_KEY` -- `CRANK_BASE_URL` -Опционально: +Эти настройки ограничивают параллельное выполнение операций и служебных задач. -- `CRANK_ADMIN_TOKEN` -- `CRANK_DEMO_SEED` -- `CRANK_CACHE_BACKEND` -- `CRANK_CACHE_URL` -- `CRANK_CACHE_DEFAULT_TTL_MS` +## Кэш -Стартовое значение для refresh published tools: +По умолчанию Crank работает без внешнего кэша: -- `CRANK_ADMIN_RATE_LIMIT_RPS=30` -- `CRANK_ADMIN_RATE_LIMIT_BURST=60` -- `CRANK_MCP_REFRESH_MS=5000` -- `CRANK_MCP_RATE_LIMIT_RPS=60` -- `CRANK_MCP_RATE_LIMIT_BURST=120` +```env +CRANK_CACHE_BACKEND=memory +``` -Стартовые значения для runtime concurrency limits: +Для Valkey или Redis: -- `CRANK_RUNTIME_MAX_CONCURRENT_UNARY=64` -- `CRANK_RUNTIME_MAX_CONCURRENT_WINDOW=16` -- `CRANK_RUNTIME_MAX_CONCURRENT_SESSIONS=16` -- `CRANK_RUNTIME_MAX_CONCURRENT_JOBS=16` +```env +CRANK_CACHE_BACKEND=valkey +CRANK_CACHE_URL=redis://valkey:6379/0 +CRANK_CACHE_DEFAULT_TTL_MS=60000 +``` -## 6. Логирование и трассировка +Внешний кэш используется для служебного краткоживущего состояния: rate limiting, replay guard и опубликованные каталоги MCP-инструментов. -Для MVP нужно использовать: +## Логи -- structured logging через `tracing`; -- correlation id для test runs и runtime execution; -- раздельные стадии ошибок: schema, mapping, adapter, external service. +- `CRANK_LOG_LEVEL` - уровень логирования, например `info`, `debug`, `warn`. -## 7. Таймауты и retries +Пример: -Рекомендуемые стартовые значения: - -- default timeout: `10s` -- retry default: `1` attempt, то есть без автоматического повтора - -Причина: - -- сначала важнее детерминированность и прозрачность; -- aggressive retries могут маскировать реальные ошибки интеграции. - -## 8. Режимы запуска - -Минимально нужны два режима: - -- local development -- demo/deployment - -Local development: - -- локальное окружение должно иметь доступ к `PostgreSQL`; -- локальный storage; -- app-level auth и bootstrap admin user через `.env`. -- при необходимости UI можно наполнить живыми demo-данными через `CRANK_DEMO_SEED=true`. - -Demo/deployment: - -- `PostgreSQL`; -- локальный или сетевой storage; -- включенная app-level auth-защита admin-api; -- стабильный `Streamable HTTP` endpoint для MCP. -- containerized runtime через `Docker` и `docker-compose`. -- optional shared cache layer через `Valkey/Redis`. -- registry-backed image rollout через Gitea Container Registry или совместимый registry. - -### Cache env - -Для optional cache/coordination layer должны быть предусмотрены: - -- `CRANK_CACHE_BACKEND` -- `CRANK_CACHE_URL` -- `CRANK_CACHE_DEFAULT_TTL_MS` - -Рекомендуемая модель: - -- `CRANK_CACHE_BACKEND=memory` по умолчанию; -- `CRANK_CACHE_BACKEND=valkey` или `redis` при наличии внешнего cache store; -- без этих переменных система должна оставаться полностью работоспособной. - -### Cache boundaries - -В платформе должны существовать два разных cache-контура: - -- `platform / coordination cache` -- `response cache` - -Первый контур хранит служебное краткоживущее состояние: - -- ingress rate limiting; -- replay guard; -- ephemeral coordination state; -- shared snapshots published MCP catalogs between instances; - -Второй контур хранит только кэшируемые ответы операций. - -Текущий безопасный runtime scope для response cache: - -- `REST GET`; - -Это не означает автоматическое кэширование всех REST-вызовов. Кэширование -разрешается только для безопасных `GET` operations без upstream auth profile. - -Эти контуры не должны смешивать ключи друг с другом. - -### Cache key isolation - -Базовое правило изоляции: - -- разные `workspace` не должны делить одни и те же cache keys; -- разные `agent` внутри одного `workspace` тоже не должны делить одни и те же response cache keys по умолчанию; -- разные `operation` внутри одного `agent` не должны попадать в общий response cache namespace. - -Стартовая модель namespace для response cache: - -- `workspace + agent + operation + operation version + request fingerprint` - -Стартовая модель namespace для platform / coordination cache: - -- `workspace + agent + cache scope + logical key` - -Это позволяет безопасно использовать один внешний `Valkey/Redis` сразу для нескольких агентов и рабочих областей без взаимного пересечения данных. - -### Auth env - -Для app-level auth нужны: - -- `CRANK_SESSION_SECRET` -- `CRANK_PASSWORD_PEPPER` -- `CRANK_BOOTSTRAP_ADMIN_EMAIL` -- `CRANK_BOOTSTRAP_ADMIN_PASSWORD` -- `CRANK_BOOTSTRAP_ADMIN_DISPLAY_NAME` - -Для secret store foundation нужен: - -- `CRANK_MASTER_KEY` - -`CRANK_MASTER_KEY` обязателен и для `admin-api`, и для `mcp-server`, потому что оба приложения -должны уметь резолвить `secret_id` в runtime. - -Для опционального demo-seed: - -- `CRANK_DEMO_SEED=true` - -В deployment workflow больше не используется монолитный `DEPLOY_ENV_FILE`. - -`.env` на сервере собирается из OpenBao. В Gitea Actions хранятся только AppRole credentials -`BAO_ADDR`, `BAO_ROLE_ID` и `BAO_SECRET_ID`, а внутри OpenBao ключи проекта -`projects/crank/runtime` совпадают с именами runtime env-переменных. Это значит, что: - -- `CRANK_MASTER_KEY` хранится в OpenBao как ключ `CRANK_MASTER_KEY`; -- `CRANK_DEMO_SEED` хранится в OpenBao как ключ `CRANK_DEMO_SEED`; -- и так же для остальных runtime-переменных. - -Для БД основной runtime-контракт теперь компонентный: - -- `POSTGRES_HOST` -- `POSTGRES_PORT` -- `POSTGRES_DB` -- `POSTGRES_USER` -- `POSTGRES_PASSWORD` -- `POSTGRES_MAX_CONNECTIONS` -- `POSTGRES_MIN_CONNECTIONS` -- `POSTGRES_ACQUIRE_TIMEOUT_MS` -- `POSTGRES_IDLE_TIMEOUT_MS` -- `POSTGRES_MAX_LIFETIME_MS` - -Для pool behavior используются явные defaults: - -- `POSTGRES_MAX_CONNECTIONS=20` -- `POSTGRES_MIN_CONNECTIONS=2` -- `POSTGRES_ACQUIRE_TIMEOUT_MS=5000` -- `POSTGRES_IDLE_TIMEOUT_MS=600000` -- `POSTGRES_MAX_LIFETIME_MS=1800000` - -Для runtime concurrency используются явные defaults: - -- `CRANK_RUNTIME_MAX_CONCURRENT_UNARY=64` -- `CRANK_RUNTIME_MAX_CONCURRENT_WINDOW=16` -- `CRANK_RUNTIME_MAX_CONCURRENT_SESSIONS=16` -- `CRANK_RUNTIME_MAX_CONCURRENT_JOBS=16` - -Для MCP transport ingress throttling используются явные defaults: - -- `CRANK_MCP_RATE_LIMIT_RPS=60` -- `CRANK_MCP_RATE_LIMIT_BURST=120` - -Для admin-api ingress throttling используются явные defaults: - -- `CRANK_ADMIN_RATE_LIMIT_RPS=30` -- `CRANK_ADMIN_RATE_LIMIT_BURST=60` - -`CRANK_DATABASE_URL` допускается только как backward-compatible fallback для локальных тестов и -переходного периода, но не как основная deployment-модель. - -## 8.1. Delivery artifacts - -Для production-like запуска проект должен поставляться с: - -- `Dockerfile` для backend приложений; -- `deploy/community/docker-compose.yml` как canonical Community deployment manifest; -- `deploy/community/.env.example` как canonical Community env template; -- optional `Valkey` service как рекомендованный, но не обязательный компонент Community deployment; -- root `docker-compose.yml` и root `.env.example` только как local development convenience files; -- healthcheck endpoints; -- reverse proxy configuration examples. - -Подробности вынесены в `docs/deployment.md`. - -## 9. Что важно не допустить - -- пути к storage, зашитые в код; -- секреты в `.yaml` exports; -- разные конфигурационные модели для local и production без причины; -- смешивание runtime config и business config operation. - -## 10. Практический итог - -До старта разработки должны быть приняты как минимум такие решения: - -- где лежит БД; -- где лежат artifacts; -- как резолвятся `secret_id` и как ротируется `CRANK_MASTER_KEY`; -- на каких bind-address запускаются `admin-api` и `mcp-server`; -- какой transport использует MCP server. +```env +CRANK_LOG_LEVEL=info +``` diff --git a/docs/secrets-and-auth.md b/docs/secrets-and-auth.md new file mode 100644 index 0000000..0ab7f8f --- /dev/null +++ b/docs/secrets-and-auth.md @@ -0,0 +1,33 @@ +# Секреты и авторизация REST API + +Crank может вызывать REST API без авторизации или с авторизацией через сохраненный секрет. + +## Секреты + +Поддерживаемые типы: + +- токен; +- логин и пароль; +- значение HTTP-заголовка; +- произвольный JSON. + +Секреты шифруются ключом `CRANK_MASTER_KEY`. После создания или ротации значение нельзя прочитать через UI или API. + +## Профили авторизации + +Профиль авторизации описывает, как применить секрет к REST-запросу: + +- `Bearer token`; +- `Basic auth`; +- API key в заголовке; +- API key в query-параметре. + +Операция хранит ссылку на профиль авторизации, а не само значение секрета. + +## Рекомендации + +- Не вставляйте токены в статические заголовки операции. +- Используйте секреты и профили авторизации для всех чувствительных данных. +- Ротируйте секрет при подозрении на утечку. +- Не экспортируйте реальные секреты вместе с YAML-конфигурациями. + diff --git a/docs/ui.md b/docs/ui.md new file mode 100644 index 0000000..df4bec6 --- /dev/null +++ b/docs/ui.md @@ -0,0 +1,54 @@ +# Веб-интерфейс + +Веб-интерфейс нужен для настройки инструментов, агентов и доступа к MCP. + +## Операции + +Операция описывает один REST endpoint как MCP-инструмент. + +В операции задаются: + +- имя инструмента; +- описание для AI-агента; +- входная схема; +- REST endpoint; +- правила преобразования входных параметров в REST-запрос; +- правила преобразования REST-ответа в результат инструмента; +- тестовый пример; +- статус публикации. + +Черновик можно редактировать и тестировать. MCP-клиенты видят только опубликованные операции, которые привязаны к опубликованному агенту. + +## Агенты + +Агент - это отдельный MCP endpoint с выбранным набором инструментов. + +Рекомендуемый подход: + +- группировать инструменты под конкретную задачу; +- не давать одному агенту слишком много инструментов; +- делать названия и описания инструментов однозначными; +- публиковать агента только после проверки операций. + +## API ключи + +API-ключ выдается на конкретного агента. Ключ позволяет MCP-клиенту: + +- открыть MCP-сессию; +- получить список инструментов агента; +- вызвать опубликованный инструмент. + +Полное значение ключа показывается только при создании. + +## Секреты + +Секреты используются для авторизации на конечных REST API. + +После сохранения значение шифруется и больше не отображается. Секрет можно ротировать или удалить, если он не используется профилем авторизации. + +## Логи и использование + +Раздел **Логи** показывает вызовы операций, ошибки маппинга, ошибки REST API и успешные ответы. + +Раздел **Использование** показывает количество вызовов, ошибки и задержки по операциям. + diff --git a/examples/mcp-smoke/rest-open-meteo.operation.json b/examples/mcp-smoke/rest-open-meteo.operation.json deleted file mode 100644 index 9e9505e..0000000 --- a/examples/mcp-smoke/rest-open-meteo.operation.json +++ /dev/null @@ -1,120 +0,0 @@ -{ - "name": "weather_current_open_meteo", - "display_name": "Get Current Weather", - "category": "weather", - "protocol": "rest", - "target": { - "kind": "rest", - "base_url": "https://api.open-meteo.com", - "method": "GET", - "path_template": "/v1/forecast", - "static_headers": {} - }, - "input_schema": { - "type": "object", - "required": true, - "fields": { - "latitude": { - "type": "number", - "required": true - }, - "longitude": { - "type": "number", - "required": true - } - } - }, - "output_schema": { - "type": "object", - "required": true, - "fields": { - "time": { - "type": "string", - "required": true - }, - "temperature_c": { - "type": "number", - "required": true - }, - "wind_speed_kmh": { - "type": "number", - "required": true - }, - "timezone": { - "type": "string", - "required": true - } - } - }, - "input_mapping": { - "rules": [ - { - "source": "$.mcp.latitude", - "target": "$.request.query.latitude", - "required": true - }, - { - "source": "$.mcp.longitude", - "target": "$.request.query.longitude", - "required": true - }, - { - "source": "$.mcp.current", - "target": "$.request.query.current", - "required": false, - "default_value": "temperature_2m,wind_speed_10m" - }, - { - "source": "$.mcp.timezone", - "target": "$.request.query.timezone", - "required": false, - "default_value": "Europe/Moscow" - } - ] - }, - "output_mapping": { - "rules": [ - { - "source": "$.response.body.current.time", - "target": "$.output.time", - "required": true - }, - { - "source": "$.response.body.current.temperature_2m", - "target": "$.output.temperature_c", - "required": true - }, - { - "source": "$.response.body.current.wind_speed_10m", - "target": "$.output.wind_speed_kmh", - "required": true - }, - { - "source": "$.response.body.timezone", - "target": "$.output.timezone", - "required": true - } - ] - }, - "execution_config": { - "timeout_ms": 10000, - "headers": {} - }, - "tool_description": { - "title": "Get Current Weather", - "description": "Fetch current weather data from Open-Meteo by latitude and longitude.", - "tags": [ - "weather", - "rest", - "smoke" - ], - "examples": [ - { - "input": { - "latitude": 55.75, - "longitude": 37.62 - } - } - ] - } -} diff --git a/examples/mcp-smoke/rest-open-meteo.operation.yaml b/examples/mcp-smoke/rest-open-meteo.operation.yaml deleted file mode 100644 index 4aff05b..0000000 --- a/examples/mcp-smoke/rest-open-meteo.operation.yaml +++ /dev/null @@ -1,95 +0,0 @@ -format_version: "1" -kind: operation -operation: - name: weather_current_open_meteo - display_name: Get Current Weather - category: weather - protocol: rest - security_level: standard - enabled: true - status: draft - version: 1 - target: - kind: rest - base_url: https://api.open-meteo.com - method: GET - path_template: /v1/forecast - static_headers: {} - input_schema: - type: object - required: true - fields: - latitude: - type: number - required: true - longitude: - type: number - required: true - output_schema: - type: object - required: true - fields: - time: - type: string - required: true - temperature_c: - type: number - required: true - wind_speed_kmh: - type: number - required: true - timezone: - type: string - required: true - input_mapping: - rules: - - source: $.mcp.latitude - target: $.request.query.latitude - required: true - - source: $.mcp.longitude - target: $.request.query.longitude - required: true - - source: $.mcp.current - target: $.request.query.current - required: false - default_value: temperature_2m,wind_speed_10m - - source: $.mcp.timezone - target: $.request.query.timezone - required: false - default_value: Europe/Moscow - output_mapping: - rules: - - source: $.response.body.current.time - target: $.output.time - required: true - - source: $.response.body.current.temperature_2m - target: $.output.temperature_c - required: true - - source: $.response.body.current.wind_speed_10m - target: $.output.wind_speed_kmh - required: true - - source: $.response.body.timezone - target: $.output.timezone - required: true - execution_config: - timeout_ms: 10000 - retry_policy: null - response_cache: null - auth_profile_ref: null - headers: {} - protocol_options: null - streaming: null - tool_description: - title: Get Current Weather - description: Fetch current weather data from Open-Meteo by latitude and longitude. - tags: - - weather - - rest - - smoke - examples: - - input: - latitude: 55.75 - longitude: 37.62 - config_export: - format_version: "1" - export_mode: portable diff --git a/examples/mcp-smoke/rest-open-meteo.test-input.json b/examples/mcp-smoke/rest-open-meteo.test-input.json deleted file mode 100644 index ddb10e4..0000000 --- a/examples/mcp-smoke/rest-open-meteo.test-input.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "latitude": 55.75, - "longitude": 37.62 -}