merge: feat/operations-workspace-contracts
This commit is contained in:
@@ -65,24 +65,14 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
cache-dependency-path: apps/ui/package-lock.json
|
||||
- name: Validate static UI files
|
||||
run: |
|
||||
test -f apps/ui/Dockerfile
|
||||
test -f apps/ui/index.html
|
||||
test -f apps/ui/nginx.conf
|
||||
|
||||
- name: Install UI dependencies
|
||||
working-directory: apps/ui
|
||||
run: npm ci
|
||||
|
||||
- name: Build UI
|
||||
working-directory: apps/ui
|
||||
run: npm run build
|
||||
|
||||
- name: Run UI tests
|
||||
working-directory: apps/ui
|
||||
run: npm run test -- --run
|
||||
- name: Build UI image
|
||||
run: docker build -f apps/ui/Dockerfile .
|
||||
|
||||
deployment:
|
||||
name: Deployment Artifacts
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
|
||||

|
||||
|
||||
Crank - это low-code платформа для публикации внешних API в виде MCP tools без написания нового backend-обработчика под каждую интеграцию. Система предоставляет единый административный UI, в котором оператор может подключать REST, GraphQL и gRPC операции, настраивать маппинг входных и выходных данных, выполнять тестовый вызов и публиковать результат как MCP tool.
|
||||
|
||||
На текущем этапе репозиторий содержит проектную документацию и архитектурные решения, которые задают границы MVP и подход к реализации.
|
||||
Crank - платформа для публикации внешних API в виде MCP tools без написания отдельного backend-кода под каждую интеграцию. Целевая модель проекта строится вокруг связки `workspace -> agent -> operations`.
|
||||
|
||||
## Цели
|
||||
|
||||
@@ -12,75 +10,80 @@ Crank - это low-code платформа для публикации внеш
|
||||
- Поддержать динамическое добавление интеграций через UI или конфигурацию.
|
||||
- Обеспечить единый сценарий работы оператора для REST, GraphQL и gRPC.
|
||||
- Нормализовать внешние протоколы в единую внутреннюю модель операции.
|
||||
- Избежать генерации и деплоя нового backend-кода при добавлении каждого нового инструмента.
|
||||
- Ограничивать набор tools на уровне конкретного агента, а не отдавать один глобальный каталог.
|
||||
- Поддержать workspace-изоляцию, platform access и observability.
|
||||
|
||||
## Состав MVP
|
||||
## Целевая модель продукта
|
||||
|
||||
- `Workspace` как tenant boundary.
|
||||
- `Operation` как интеграционный контракт.
|
||||
- `Agent` как curated MCP surface для LLM.
|
||||
- Поддержка REST для `GET`, `POST`, `PUT`, `PATCH` и `DELETE`.
|
||||
- Поддержка GraphQL для `query` и `mutation` на основе шаблонов и переменных.
|
||||
- Поддержка GraphQL для `query` и `mutation`.
|
||||
- Поддержка только unary-методов gRPC.
|
||||
- Загрузка примеров `JSON` для ускоренного создания схем и чернового маппинга.
|
||||
- Загрузка `.proto` файлов или descriptor set для обнаружения схемы gRPC.
|
||||
- Импорт и экспорт конфигураций операций в `YAML`.
|
||||
- Использование `JSONPath` для точечного маппинга вложенных параметров и ответа.
|
||||
- Настройка маппинга запроса и ответа через UI.
|
||||
- Публикация tools в MCP без пересборки backend.
|
||||
- Platform API keys и membership layer.
|
||||
- Observability: invocation logs, usage aggregates, latency/error metrics.
|
||||
- Импорт и экспорт operation-конфигураций в `YAML`.
|
||||
- Использование `JSONPath` для точечного маппинга.
|
||||
|
||||
## Структура документации
|
||||
|
||||
- `docs/architecture.md` - архитектура системы, модули, потоки данных и стек.
|
||||
- `docs/module-decomposition.md` - детальная декомпозиция crates и внутренних модулей.
|
||||
- `docs/data-model.md` - формальная модель данных и JSON-структуры сущностей.
|
||||
- `docs/database-schema.md` - схема БД, связи и versioning конфигураций.
|
||||
- `docs/admin-api.md` - HTTP-контракты административного API.
|
||||
- `docs/diagrams.md` - структурные диаграммы компонентов, сущностей, БД и потоков.
|
||||
- `docs/mcp-interface.md` - транспорт и контракт публикации MCP tools.
|
||||
- `docs/testing-strategy.md` - стратегия тестирования до и во время разработки.
|
||||
- `docs/runtime-config.md` - конфигурация окружения, storage и секретов.
|
||||
- `docs/deployment.md` - контейнерный деплой, reverse proxy и CI/CD.
|
||||
- `docs/demo-runbook.md` - пошаговый сценарий локального запуска и воспроизводимого демо.
|
||||
- `docs/rust-design.md` - распределение методов, `impl`, `trait` и service-слоя без god-struct.
|
||||
- `docs/development-rules.md` - правила разработки, TDD-процесс и git workflow.
|
||||
- `docs/rust-code-rules.md` - Rust-specific правила кода, toolchain и linting.
|
||||
- `docs/implementation-plan.md` - последовательность модулей и фич по этапам реализации.
|
||||
- `docs/protocols/rest.md` - функциональные требования и ограничения для REST.
|
||||
- `docs/protocols/graphql.md` - функциональные требования и ограничения для GraphQL.
|
||||
- `docs/protocols/grpc.md` - функциональные требования и ограничения для gRPC.
|
||||
- `docs/architecture.md` - целевая архитектура системы.
|
||||
- `docs/as-is-to-be.md` - переход `as is -> to be`, page-by-page gap analysis и архитектурные конфликты.
|
||||
- `docs/backend-gap-plan.md` - конкретный backend-план: сущности, API, БД и порядок реализации.
|
||||
- `docs/operations-workspace-contracts.md` - точные `workspace-scoped` контракты для экранов `Operations` и `Wizard`.
|
||||
- `docs/module-decomposition.md` - декомпозиция crates и модулей.
|
||||
- `docs/data-model.md` - целевая модель данных.
|
||||
- `docs/database-schema.md` - целевая схема БД.
|
||||
- `docs/admin-api.md` - целевые HTTP-контракты административного API.
|
||||
- `docs/diagrams.md` - диаграммы компонентов, сущностей и БД.
|
||||
- `docs/mcp-interface.md` - модель MCP transport и agent-scoped publishing.
|
||||
- `docs/testing-strategy.md` - стратегия тестирования.
|
||||
- `docs/runtime-config.md` - конфигурация окружения.
|
||||
- `docs/deployment.md` - деплой, reverse proxy и CI/CD.
|
||||
- `docs/demo-runbook.md` - демонстрационный сценарий.
|
||||
- `docs/rust-design.md` - правила распределения поведения в Rust.
|
||||
- `docs/development-rules.md` - правила разработки и workflow.
|
||||
- `docs/rust-code-rules.md` - Rust-specific coding rules.
|
||||
- `docs/implementation-plan.md` - порядок перехода от текущего состояния к целевой модели.
|
||||
- `docs/protocols/rest.md` - требования и ограничения для REST.
|
||||
- `docs/protocols/graphql.md` - требования и ограничения для GraphQL.
|
||||
- `docs/protocols/grpc.md` - требования и ограничения для gRPC.
|
||||
|
||||
## Ключевая идея продукта
|
||||
|
||||
Система строится вокруг унифицированной сущности `Operation`. Каждая операция описывает:
|
||||
Система строится вокруг трех уровней:
|
||||
|
||||
- внешний протокол,
|
||||
- целевой endpoint или метод,
|
||||
- входную схему,
|
||||
- правила маппинга входных данных,
|
||||
- параметры выполнения,
|
||||
- правила маппинга выходных данных,
|
||||
- `Workspace` - граница данных и доступа команды.
|
||||
- `Agent` - curated MCP endpoint для конкретного сценария LLM.
|
||||
- `Operation` - низкоуровневый интеграционный контракт.
|
||||
|
||||
`Operation` описывает:
|
||||
|
||||
- внешний протокол;
|
||||
- целевой endpoint или метод;
|
||||
- входную схему;
|
||||
- правила маппинга входных данных;
|
||||
- параметры выполнения;
|
||||
- правила маппинга выходных данных;
|
||||
- метаданные MCP tool.
|
||||
|
||||
За счет этого MCP runtime работает с единой внутренней моделью, а протокольные адаптеры уже выполняют конкретные вызовы REST, GraphQL или gRPC.
|
||||
|
||||
Для GraphQL это означает, что в MCP публикуется не "универсальный GraphQL endpoint", а конкретная операция с фиксированным шаблоном запроса, фиксированным набором входных параметров и предсказуемой структурой ответа.
|
||||
|
||||
Для упрощения настройки оператор может загружать примеры входного и выходного `JSON`, а для gRPC - `.proto` или descriptor set. На основе этих артефактов система строит черновую схему и стартовый маппинг, который затем вручную уточняется через `JSONPath`.
|
||||
|
||||
Конфигурации операций должны импортироваться и экспортироваться в `YAML`, чтобы их можно было переносить между окружениями, хранить в git и редактировать вне UI.
|
||||
`Agent` собирает ограниченный набор опубликованных операций в одну MCP-поверхность. Именно это решает проблему, когда один агент теряется в слишком большом наборе tools.
|
||||
|
||||
## CI/CD статус
|
||||
|
||||
В репозитории настроены:
|
||||
|
||||
- `CI` для Rust, UI и deployment artifacts;
|
||||
- `CI` для Rust, UI container и deployment artifacts;
|
||||
- `CD`, который запускается после успешного `CI` на `main` или вручную;
|
||||
- containerized production-like deployment через `docker compose`.
|
||||
- containerized deployment через `docker compose`.
|
||||
|
||||
## Поддерживаемые протоколы
|
||||
|
||||
В MVP платформа ориентируется на три основных протокольных сценария интеграции:
|
||||
В целевой модели платформа ориентируется на:
|
||||
|
||||
- REST
|
||||
- GraphQL
|
||||
- gRPC
|
||||
|
||||
`SOAP` сознательно не входит в MVP. Он остается актуальным для части корпоративных и государственных интеграций, но требует отдельного адаптера с поддержкой WSDL, XML Schema, SOAP envelope, namespaces и XML-oriented mapping. Для первой версии это слишком большой отдельный пласт сложности.
|
||||
`SOAP` сознательно не входит в текущий scope.
|
||||
|
||||
@@ -2,22 +2,25 @@
|
||||
|
||||
## Current
|
||||
|
||||
### `feat/crank-rebrand`
|
||||
### `feat/operations-workspace-contracts`
|
||||
|
||||
Status: completed
|
||||
|
||||
DoD:
|
||||
|
||||
- проект переименован в `Crank` в документации, UI и package metadata
|
||||
- workspace и crate package names согласованы с новым брендом
|
||||
- `README.md` использует `Crank.png` как главное изображение
|
||||
- `origin` указывает на `git@github.com:bsodfather/crank.git`
|
||||
- Rust workspace и UI build остаются зелеными
|
||||
- `workspace-scoped` контракты для `Operations` и `Wizard` зафиксированы
|
||||
- определены точные DTO и lifecycle semantics для operations
|
||||
- `PATCH/DELETE/ARCHIVE` и wizard DTO shape документированы
|
||||
|
||||
## Next
|
||||
|
||||
- `feat/demo-assets`
|
||||
- `feat/workspace-foundation`
|
||||
|
||||
## Backlog
|
||||
|
||||
- `feat/workspace-foundation`
|
||||
- `feat/agent-publishing`
|
||||
- `feat/platform-access`
|
||||
- `feat/observability-api`
|
||||
- `feat/alpine-ui`
|
||||
- `feat/demo-assets`
|
||||
|
||||
+2
-10
@@ -1,15 +1,7 @@
|
||||
FROM node:22-alpine AS build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY apps/ui/package.json apps/ui/package-lock.json ./
|
||||
RUN npm ci
|
||||
COPY apps/ui/ ./
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:1.27-alpine
|
||||
|
||||
COPY apps/ui/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
COPY apps/ui/index.html /usr/share/nginx/html/index.html
|
||||
COPY Crank.png /usr/share/nginx/html/Crank.png
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
+84
-2
@@ -4,9 +4,91 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Crank Console</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light;
|
||||
font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
|
||||
background: #eef3f7;
|
||||
color: #162433;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background:
|
||||
radial-gradient(circle at top, rgba(0, 148, 134, 0.12), transparent 28rem),
|
||||
linear-gradient(180deg, #f5f8fb 0%, #ebf1f6 100%);
|
||||
}
|
||||
|
||||
main {
|
||||
width: min(42rem, calc(100vw - 3rem));
|
||||
padding: 3rem;
|
||||
border-radius: 1.5rem;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: 1px solid rgba(22, 36, 51, 0.08);
|
||||
box-shadow: 0 24px 60px rgba(22, 36, 51, 0.12);
|
||||
}
|
||||
|
||||
img {
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
display: block;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 0.75rem;
|
||||
font-size: clamp(2rem, 5vw, 3rem);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 1rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
color: #516070;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.45rem 0.8rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.03em;
|
||||
text-transform: uppercase;
|
||||
color: #0f766e;
|
||||
background: rgba(15, 118, 110, 0.12);
|
||||
}
|
||||
|
||||
.muted {
|
||||
font-size: 0.95rem;
|
||||
color: #6a7b8d;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
<main>
|
||||
<img src="/Crank.png" alt="Crank logo" />
|
||||
<div class="badge">UI replacement in progress</div>
|
||||
<h1>Crank Console</h1>
|
||||
<p>
|
||||
The legacy React/Vite UI has been removed. A new operator console is being integrated on top of the same
|
||||
deployment path.
|
||||
</p>
|
||||
<p class="muted">
|
||||
Backend services, MCP endpoints and deployment contracts remain intact. Replace this static entrypoint with
|
||||
the new Alpine.js UI when it is ready.
|
||||
</p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Generated
-3266
File diff suppressed because it is too large
Load Diff
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"name": "crank-ui",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc --noEmit -p tsconfig.app.json && tsc --noEmit -p tsconfig.node.json && vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@tanstack/react-query": "^5.90.5",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-hook-form": "^7.62.0",
|
||||
"react-router-dom": "^7.9.3",
|
||||
"zod": "^4.1.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^6.8.0",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/react": "^19.1.16",
|
||||
"@types/react-dom": "^19.1.9",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"jsdom": "^27.0.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.1.7",
|
||||
"vitest": "^3.2.4"
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ReactNode, useState } from "react";
|
||||
|
||||
type AppProvidersProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export function AppProviders({ children }: AppProvidersProps) {
|
||||
const [queryClient] = useState(
|
||||
() =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 15_000,
|
||||
retry: 1,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom";
|
||||
|
||||
import { AppShell } from "../shared/ui/app-shell";
|
||||
import { OperationCreatePage } from "../pages/operation-create/page";
|
||||
import { OperationDetailPage } from "../pages/operation-detail/page";
|
||||
import { OperationListPage } from "../pages/operation-list/page";
|
||||
|
||||
export function AppRouter() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<AppShell>
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate to="/operations" replace />} />
|
||||
<Route path="/operations" element={<OperationListPage />} />
|
||||
<Route path="/operations/new" element={<OperationCreatePage />} />
|
||||
<Route
|
||||
path="/operations/:operationId"
|
||||
element={<OperationDetailPage />}
|
||||
/>
|
||||
</Routes>
|
||||
</AppShell>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
import {
|
||||
getJson,
|
||||
getText,
|
||||
postBytes,
|
||||
postJson,
|
||||
postText,
|
||||
} from "../../shared/api/client";
|
||||
import type {
|
||||
AuthProfile,
|
||||
DraftGenerationResult,
|
||||
GrpcServiceSummary,
|
||||
OperationRecord,
|
||||
OperationSummary,
|
||||
TestRunResult,
|
||||
} from "./types";
|
||||
|
||||
export function listOperations() {
|
||||
return getJson<{ items: OperationSummary[] }>("/api/admin/operations");
|
||||
}
|
||||
|
||||
export function getOperation(operationId: string) {
|
||||
return getJson<OperationSummary>(`/api/admin/operations/${operationId}`);
|
||||
}
|
||||
|
||||
export function getOperationVersion(operationId: string, version: number) {
|
||||
return getJson<OperationRecord>(
|
||||
`/api/admin/operations/${operationId}/versions/${version}`,
|
||||
);
|
||||
}
|
||||
|
||||
export function createOperation(payload: unknown) {
|
||||
return postJson<{
|
||||
operation_id: string;
|
||||
version: number;
|
||||
status: string;
|
||||
}>("/api/admin/operations", payload);
|
||||
}
|
||||
|
||||
export function publishOperation(operationId: string, version: number) {
|
||||
return postJson<{ published_version: number; published_at: string }>(
|
||||
`/api/admin/operations/${operationId}/publish`,
|
||||
{ version },
|
||||
);
|
||||
}
|
||||
|
||||
export function runOperationTest(
|
||||
operationId: string,
|
||||
version: number,
|
||||
input: unknown,
|
||||
) {
|
||||
return postJson<TestRunResult>(`/api/admin/operations/${operationId}/test-runs`, {
|
||||
version,
|
||||
input,
|
||||
});
|
||||
}
|
||||
|
||||
export function uploadInputJsonSample(operationId: string, payload: unknown) {
|
||||
return postJson(`/api/admin/operations/${operationId}/samples/input-json`, payload);
|
||||
}
|
||||
|
||||
export function uploadOutputJsonSample(operationId: string, payload: unknown) {
|
||||
return postJson(
|
||||
`/api/admin/operations/${operationId}/samples/output-json`,
|
||||
payload,
|
||||
);
|
||||
}
|
||||
|
||||
export function generateDraft(operationId: string) {
|
||||
return postJson<DraftGenerationResult>(
|
||||
`/api/admin/operations/${operationId}/drafts/generate`,
|
||||
{
|
||||
sources: ["input_json_sample", "output_json_sample"],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function exportOperationYaml(operationId: string, version?: number) {
|
||||
const query = version === undefined ? "" : `?version=${version}`;
|
||||
return getText(`/api/admin/operations/${operationId}/export${query}`);
|
||||
}
|
||||
|
||||
export function importOperationYaml(yamlText: string, mode: "create" | "upsert") {
|
||||
return postText<{ operation_id: string; version: number; import_mode: string }>(
|
||||
`/api/admin/operations/import?mode=${mode}`,
|
||||
yamlText,
|
||||
);
|
||||
}
|
||||
|
||||
export function listAuthProfiles() {
|
||||
return getJson<{ items: AuthProfile[] }>("/api/admin/auth-profiles");
|
||||
}
|
||||
|
||||
export function uploadProtoDescriptor(
|
||||
operationId: string,
|
||||
fileName: string,
|
||||
bytes: ArrayBuffer,
|
||||
) {
|
||||
return postBytes<{ descriptor_id: string; version: number }>(
|
||||
`/api/admin/operations/${operationId}/descriptors/proto`,
|
||||
bytes,
|
||||
{
|
||||
"Content-Type": "application/octet-stream",
|
||||
"x-file-name": fileName,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function uploadDescriptorSet(
|
||||
operationId: string,
|
||||
fileName: string,
|
||||
bytes: ArrayBuffer,
|
||||
) {
|
||||
return postBytes<{ descriptor_id: string; version: number }>(
|
||||
`/api/admin/operations/${operationId}/descriptors/descriptor-set`,
|
||||
bytes,
|
||||
{
|
||||
"Content-Type": "application/octet-stream",
|
||||
"x-file-name": fileName,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function listGrpcServices(operationId: string, version?: number) {
|
||||
const query = version === undefined ? "" : `?version=${version}`;
|
||||
return getJson<{ services: GrpcServiceSummary[] }>(
|
||||
`/api/admin/operations/${operationId}/grpc/services${query}`,
|
||||
);
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
export type Protocol = "rest" | "graphql" | "grpc";
|
||||
export type OperationStatus = "draft" | "testing" | "published" | "archived";
|
||||
|
||||
export type RestTarget = {
|
||||
kind: "rest";
|
||||
base_url: string;
|
||||
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
||||
path_template: string;
|
||||
static_headers?: Record<string, string>;
|
||||
};
|
||||
|
||||
export type GraphqlTarget = {
|
||||
kind: "graphql";
|
||||
endpoint: string;
|
||||
operation_type: "query" | "mutation";
|
||||
operation_name: string;
|
||||
query_template: string;
|
||||
response_path: string;
|
||||
};
|
||||
|
||||
export type GrpcTarget = {
|
||||
kind: "grpc";
|
||||
server_addr: string;
|
||||
package: string;
|
||||
service: string;
|
||||
method: string;
|
||||
descriptor_ref: string;
|
||||
descriptor_set_b64: string;
|
||||
};
|
||||
|
||||
export type OperationTarget = RestTarget | GraphqlTarget | GrpcTarget;
|
||||
|
||||
export type OperationSummary = {
|
||||
id: string;
|
||||
name: string;
|
||||
display_name: string;
|
||||
protocol: Protocol;
|
||||
status: OperationStatus;
|
||||
current_draft_version: number;
|
||||
latest_published_version: number | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
published_at: string | null;
|
||||
};
|
||||
|
||||
export type OperationRecord = {
|
||||
operation_id: string;
|
||||
version: number;
|
||||
status: OperationStatus;
|
||||
change_note: string | null;
|
||||
created_at: string;
|
||||
created_by: string | null;
|
||||
snapshot: OperationSnapshot;
|
||||
};
|
||||
|
||||
export type OperationSnapshot = {
|
||||
id: string;
|
||||
name: string;
|
||||
display_name: string;
|
||||
protocol: Protocol;
|
||||
status: OperationStatus;
|
||||
version: number;
|
||||
target: OperationTarget;
|
||||
input_schema: unknown;
|
||||
output_schema: unknown;
|
||||
input_mapping: unknown;
|
||||
output_mapping: unknown;
|
||||
execution_config: unknown;
|
||||
tool_description: {
|
||||
title: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
examples: Array<{ input: unknown }>;
|
||||
};
|
||||
};
|
||||
|
||||
export type DraftGenerationResult = {
|
||||
generated_draft: {
|
||||
status: string;
|
||||
source_types: string[];
|
||||
generated_at: string | null;
|
||||
input_schema_generated: boolean;
|
||||
output_schema_generated: boolean;
|
||||
input_mapping_generated: boolean;
|
||||
output_mapping_generated: boolean;
|
||||
warnings: string[];
|
||||
};
|
||||
input_schema: unknown;
|
||||
output_schema: unknown;
|
||||
input_mapping: unknown;
|
||||
output_mapping: unknown;
|
||||
};
|
||||
|
||||
export type TestRunResult = {
|
||||
ok: boolean;
|
||||
request_preview: unknown;
|
||||
response_preview: unknown;
|
||||
errors: Array<{ code: string; message: string }>;
|
||||
};
|
||||
|
||||
export type AuthProfile = {
|
||||
id: string;
|
||||
name: string;
|
||||
kind: string;
|
||||
config: unknown;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type GrpcMethodSummary = {
|
||||
name: string;
|
||||
kind: string;
|
||||
input_schema: unknown;
|
||||
output_schema: unknown;
|
||||
};
|
||||
|
||||
export type GrpcServiceSummary = {
|
||||
package: string;
|
||||
service: string;
|
||||
methods: GrpcMethodSummary[];
|
||||
};
|
||||
@@ -1,160 +0,0 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
|
||||
import {
|
||||
listGrpcServices,
|
||||
uploadDescriptorSet,
|
||||
uploadProtoDescriptor,
|
||||
} from "../../entities/operation/api";
|
||||
import type { GrpcServiceSummary } from "../../entities/operation/types";
|
||||
import { ApiError } from "../../shared/api/client";
|
||||
|
||||
type GrpcDescriptorPanelProps = {
|
||||
operationId: string;
|
||||
version: number;
|
||||
};
|
||||
|
||||
type UploadKind = "proto" | "descriptor-set";
|
||||
|
||||
function descriptorSubtitle(service: GrpcServiceSummary) {
|
||||
return service.package.length > 0
|
||||
? `${service.package}.${service.service}`
|
||||
: service.service;
|
||||
}
|
||||
|
||||
export function GrpcDescriptorPanel({
|
||||
operationId,
|
||||
version,
|
||||
}: GrpcDescriptorPanelProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const [uploadError, setUploadError] = useState<string | null>(null);
|
||||
|
||||
const servicesQuery = useQuery({
|
||||
queryKey: ["grpc-services", operationId, version],
|
||||
queryFn: () => listGrpcServices(operationId, version),
|
||||
enabled: operationId.length > 0 && version > 0,
|
||||
});
|
||||
|
||||
const uploadMutation = useMutation({
|
||||
mutationFn: async ({
|
||||
file,
|
||||
kind,
|
||||
}: {
|
||||
file: File;
|
||||
kind: UploadKind;
|
||||
}) => {
|
||||
const bytes = await file.arrayBuffer();
|
||||
if (kind === "proto") {
|
||||
return uploadProtoDescriptor(operationId, file.name, bytes);
|
||||
}
|
||||
|
||||
return uploadDescriptorSet(operationId, file.name, bytes);
|
||||
},
|
||||
onSuccess: async () => {
|
||||
setUploadError(null);
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["grpc-services", operationId, version],
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
setUploadError(
|
||||
error instanceof ApiError ? error.message : "Descriptor upload failed",
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
async function handleFileUpload(
|
||||
file: File | undefined,
|
||||
kind: UploadKind,
|
||||
) {
|
||||
if (file === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
setUploadError(null);
|
||||
uploadMutation.mutate({ file, kind });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="stack-layout">
|
||||
<div className="upload-grid">
|
||||
<label className="field-block">
|
||||
<span>Upload `.proto`</span>
|
||||
<small className="field-hint">
|
||||
Stores the source artifact alongside the current draft version.
|
||||
</small>
|
||||
<input
|
||||
type="file"
|
||||
accept=".proto"
|
||||
onChange={(event) => {
|
||||
void handleFileUpload(event.target.files?.[0], "proto");
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<label className="field-block">
|
||||
<span>Upload descriptor set</span>
|
||||
<small className="field-hint">
|
||||
Enables unary service discovery and schema extraction for the current version.
|
||||
</small>
|
||||
<input
|
||||
type="file"
|
||||
accept=".bin,.pb,.desc"
|
||||
onChange={(event) => {
|
||||
void handleFileUpload(event.target.files?.[0], "descriptor-set");
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{uploadMutation.isPending ? (
|
||||
<div className="feedback-card">Uploading descriptor artifact...</div>
|
||||
) : null}
|
||||
{uploadError ? (
|
||||
<div className="feedback-card feedback-error">{uploadError}</div>
|
||||
) : null}
|
||||
|
||||
{servicesQuery.data?.services.length ? (
|
||||
<div className="service-grid">
|
||||
{servicesQuery.data.services.map((service) => (
|
||||
<article className="service-card" key={descriptorSubtitle(service)}>
|
||||
<div className="service-card-header">
|
||||
<div>
|
||||
<h3>{service.service}</h3>
|
||||
<p>{descriptorSubtitle(service)}</p>
|
||||
</div>
|
||||
<span className="status-pill status-testing">
|
||||
{service.methods.length} methods
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="service-method-list">
|
||||
{service.methods.map((method) => (
|
||||
<div className="service-method" key={method.name}>
|
||||
<div className="service-method-top">
|
||||
<strong>{method.name}</strong>
|
||||
<span className="protocol-pill protocol-grpc">{method.kind}</span>
|
||||
</div>
|
||||
<div className="result-grid">
|
||||
<div className="result-panel">
|
||||
<h3>Input schema</h3>
|
||||
<pre>{JSON.stringify(method.input_schema, null, 2)}</pre>
|
||||
</div>
|
||||
<div className="result-panel">
|
||||
<h3>Output schema</h3>
|
||||
<pre>{JSON.stringify(method.output_schema, null, 2)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="feedback-card">
|
||||
Upload a descriptor set to inspect unary services and message schemas.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { buildOperationPayload, defaultOperationFormValues } from "./model";
|
||||
|
||||
describe("buildOperationPayload", () => {
|
||||
it("creates a REST operation payload from form values", () => {
|
||||
const payload = buildOperationPayload({
|
||||
...defaultOperationFormValues,
|
||||
restStaticHeadersText: JSON.stringify(
|
||||
{
|
||||
"x-app-source": "crank",
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
executionHeadersText: JSON.stringify(
|
||||
{
|
||||
"x-trace-id": "trace-123",
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
});
|
||||
|
||||
expect(payload.protocol).toBe("rest");
|
||||
expect(payload.target.kind).toBe("rest");
|
||||
expect(payload.target.method).toBe("POST");
|
||||
expect(payload.target.static_headers).toEqual({
|
||||
"x-app-source": "crank",
|
||||
});
|
||||
expect(payload.execution_config.headers).toEqual({
|
||||
"x-trace-id": "trace-123",
|
||||
});
|
||||
expect(payload.input_mapping).toEqual({
|
||||
rules: [
|
||||
{
|
||||
source: "$.mcp.email",
|
||||
target: "$.request.body.email",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("creates a GraphQL operation payload from form values", () => {
|
||||
const payload = buildOperationPayload({
|
||||
...defaultOperationFormValues,
|
||||
protocol: "graphql",
|
||||
name: "crm_create_lead_graphql",
|
||||
displayName: "Create Lead (GraphQL)",
|
||||
toolTitle: "Create CRM lead through GraphQL",
|
||||
inputMappingText: JSON.stringify(
|
||||
{
|
||||
rules: [
|
||||
{
|
||||
source: "$.mcp.email",
|
||||
target: "$.request.variables.email",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
outputMappingText: JSON.stringify(
|
||||
{
|
||||
rules: [
|
||||
{
|
||||
source: "$.response.body.data.createLead.id",
|
||||
target: "$.output.id",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
});
|
||||
|
||||
expect(payload.protocol).toBe("graphql");
|
||||
expect(payload.target.kind).toBe("graphql");
|
||||
expect(payload.target.operation_type).toBe("mutation");
|
||||
expect(payload.target.response_path).toBe("$.response.body.data.createLead");
|
||||
});
|
||||
|
||||
it("creates a gRPC operation payload from form values", () => {
|
||||
const payload = buildOperationPayload({
|
||||
...defaultOperationFormValues,
|
||||
protocol: "grpc",
|
||||
name: "crm_create_lead_grpc",
|
||||
displayName: "Create Lead (gRPC)",
|
||||
toolTitle: "Create CRM lead through gRPC",
|
||||
grpcDescriptorSetB64: "ZGVzY3JpcHRvcg==",
|
||||
inputMappingText: JSON.stringify(
|
||||
{
|
||||
rules: [
|
||||
{
|
||||
source: "$.mcp.email",
|
||||
target: "$.request.grpc.email",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
outputMappingText: JSON.stringify(
|
||||
{
|
||||
rules: [
|
||||
{
|
||||
source: "$.response.body.id",
|
||||
target: "$.output.id",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
});
|
||||
|
||||
expect(payload.protocol).toBe("grpc");
|
||||
expect(payload.target.kind).toBe("grpc");
|
||||
expect(payload.target.descriptor_ref).toBe("desc_lead_service");
|
||||
expect(payload.target.descriptor_set_b64).toBe("ZGVzY3JpcHRvcg==");
|
||||
});
|
||||
|
||||
it("throws when JSON fields are invalid", () => {
|
||||
expect(() =>
|
||||
buildOperationPayload({
|
||||
...defaultOperationFormValues,
|
||||
inputSchemaText: "{invalid",
|
||||
}),
|
||||
).toThrow(/Input schema contains invalid JSON/);
|
||||
});
|
||||
|
||||
it("throws when header values are not string maps", () => {
|
||||
expect(() =>
|
||||
buildOperationPayload({
|
||||
...defaultOperationFormValues,
|
||||
executionHeadersText: JSON.stringify({ "x-trace-id": 42 }, null, 2),
|
||||
}),
|
||||
).toThrow(/Execution headers.x-trace-id must be a string value/);
|
||||
});
|
||||
});
|
||||
@@ -1,430 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { safeParseJson } from "../../shared/lib/json";
|
||||
|
||||
const jsonTextSchema = z.string().min(2, "JSON payload is required");
|
||||
|
||||
export const operationFormSchema = z
|
||||
.object({
|
||||
protocol: z.enum(["rest", "graphql", "grpc"]),
|
||||
name: z
|
||||
.string()
|
||||
.min(3, "Name is too short")
|
||||
.regex(/^[a-z0-9_]+$/, "Use lowercase snake_case"),
|
||||
displayName: z.string().min(3, "Display name is too short"),
|
||||
toolTitle: z.string().min(3, "Tool title is too short"),
|
||||
toolDescription: z.string().min(8, "Tool description is too short"),
|
||||
restBaseUrl: z.string(),
|
||||
restMethod: z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
|
||||
restPathTemplate: z.string(),
|
||||
restStaticHeadersText: jsonTextSchema,
|
||||
graphqlEndpoint: z.string(),
|
||||
graphqlOperationType: z.enum(["query", "mutation"]),
|
||||
graphqlOperationName: z.string(),
|
||||
graphqlQueryTemplate: z.string(),
|
||||
graphqlResponsePath: z.string(),
|
||||
grpcServerAddr: z.string(),
|
||||
grpcPackage: z.string(),
|
||||
grpcService: z.string(),
|
||||
grpcMethod: z.string(),
|
||||
grpcDescriptorRef: z.string(),
|
||||
grpcDescriptorSetB64: z.string(),
|
||||
inputSchemaText: jsonTextSchema,
|
||||
outputSchemaText: jsonTextSchema,
|
||||
inputMappingText: jsonTextSchema,
|
||||
outputMappingText: jsonTextSchema,
|
||||
executionHeadersText: jsonTextSchema,
|
||||
executionConfigText: jsonTextSchema,
|
||||
})
|
||||
.superRefine((values, context) => {
|
||||
if (values.protocol === "rest") {
|
||||
if (!z.string().url().safeParse(values.restBaseUrl).success) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["restBaseUrl"],
|
||||
message: "Base URL must be valid",
|
||||
});
|
||||
}
|
||||
|
||||
if (values.restPathTemplate.trim().length === 0) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["restPathTemplate"],
|
||||
message: "Path is required",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (values.protocol === "graphql") {
|
||||
if (!z.string().url().safeParse(values.graphqlEndpoint).success) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["graphqlEndpoint"],
|
||||
message: "Endpoint must be valid",
|
||||
});
|
||||
}
|
||||
|
||||
if (values.graphqlOperationName.trim().length < 2) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["graphqlOperationName"],
|
||||
message: "Operation name is required",
|
||||
});
|
||||
}
|
||||
|
||||
if (values.graphqlQueryTemplate.trim().length < 8) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["graphqlQueryTemplate"],
|
||||
message: "Query template is too short",
|
||||
});
|
||||
}
|
||||
|
||||
if (values.graphqlResponsePath.trim().length < 4) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["graphqlResponsePath"],
|
||||
message: "Response path is required",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (values.protocol === "grpc") {
|
||||
const requiredFields: Array<[keyof typeof values, string]> = [
|
||||
["grpcServerAddr", "Server address is required"],
|
||||
["grpcPackage", "Package is required"],
|
||||
["grpcService", "Service is required"],
|
||||
["grpcMethod", "Method is required"],
|
||||
["grpcDescriptorRef", "Descriptor reference is required"],
|
||||
["grpcDescriptorSetB64", "Descriptor set base64 is required"],
|
||||
];
|
||||
|
||||
for (const [fieldName, message] of requiredFields) {
|
||||
if (values[fieldName].trim().length === 0) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: [fieldName],
|
||||
message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export type OperationFormValues = z.infer<typeof operationFormSchema>;
|
||||
|
||||
function parseStringMap(raw: string, fieldName: string) {
|
||||
const value = safeParseJson<Record<string, unknown>>(raw, fieldName);
|
||||
|
||||
if (value === null || Array.isArray(value) || typeof value !== "object") {
|
||||
throw new Error(`${fieldName} must be a JSON object`);
|
||||
}
|
||||
|
||||
for (const [key, entry] of Object.entries(value)) {
|
||||
if (typeof entry !== "string") {
|
||||
throw new Error(`${fieldName}.${key} must be a string value`);
|
||||
}
|
||||
}
|
||||
|
||||
return value as Record<string, string>;
|
||||
}
|
||||
|
||||
function defaultInputSchemaText() {
|
||||
return JSON.stringify(
|
||||
{
|
||||
type: "object",
|
||||
required: true,
|
||||
nullable: false,
|
||||
fields: {
|
||||
email: {
|
||||
type: "string",
|
||||
required: true,
|
||||
nullable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
}
|
||||
|
||||
function defaultOutputSchemaText() {
|
||||
return JSON.stringify(
|
||||
{
|
||||
type: "object",
|
||||
required: true,
|
||||
nullable: false,
|
||||
fields: {
|
||||
id: {
|
||||
type: "string",
|
||||
required: true,
|
||||
nullable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
}
|
||||
|
||||
function defaultRestInputMappingText() {
|
||||
return JSON.stringify(
|
||||
{
|
||||
rules: [
|
||||
{
|
||||
source: "$.mcp.email",
|
||||
target: "$.request.body.email",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
}
|
||||
|
||||
function defaultRestOutputMappingText() {
|
||||
return JSON.stringify(
|
||||
{
|
||||
rules: [
|
||||
{
|
||||
source: "$.response.body.id",
|
||||
target: "$.output.id",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
}
|
||||
|
||||
function defaultGraphqlInputMappingText() {
|
||||
return JSON.stringify(
|
||||
{
|
||||
rules: [
|
||||
{
|
||||
source: "$.mcp.email",
|
||||
target: "$.request.variables.email",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
}
|
||||
|
||||
function defaultGraphqlOutputMappingText() {
|
||||
return JSON.stringify(
|
||||
{
|
||||
rules: [
|
||||
{
|
||||
source: "$.response.body.data.createLead.id",
|
||||
target: "$.output.id",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
}
|
||||
|
||||
function defaultGrpcInputMappingText() {
|
||||
return JSON.stringify(
|
||||
{
|
||||
rules: [
|
||||
{
|
||||
source: "$.mcp.email",
|
||||
target: "$.request.grpc.email",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
}
|
||||
|
||||
function defaultGrpcOutputMappingText() {
|
||||
return JSON.stringify(
|
||||
{
|
||||
rules: [
|
||||
{
|
||||
source: "$.response.body.id",
|
||||
target: "$.output.id",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
}
|
||||
|
||||
function defaultExecutionConfigText() {
|
||||
return JSON.stringify(
|
||||
{
|
||||
timeout_ms: 10000,
|
||||
headers: {},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
}
|
||||
|
||||
export const defaultOperationFormValues: OperationFormValues = {
|
||||
protocol: "rest",
|
||||
name: "crm_create_lead",
|
||||
displayName: "Create Lead",
|
||||
toolTitle: "Create CRM lead",
|
||||
toolDescription: "Creates a CRM lead from MCP input fields.",
|
||||
restBaseUrl: "https://api.example.com",
|
||||
restMethod: "POST",
|
||||
restPathTemplate: "/v1/leads",
|
||||
restStaticHeadersText: JSON.stringify({}, null, 2),
|
||||
graphqlEndpoint: "https://api.example.com/graphql",
|
||||
graphqlOperationType: "mutation",
|
||||
graphqlOperationName: "CreateLead",
|
||||
graphqlQueryTemplate: `mutation CreateLead($email: String!) {
|
||||
createLead(email: $email) {
|
||||
id
|
||||
status
|
||||
}
|
||||
}`,
|
||||
graphqlResponsePath: "$.response.body.data.createLead",
|
||||
grpcServerAddr: "https://grpc.example.com",
|
||||
grpcPackage: "crm.v1",
|
||||
grpcService: "LeadService",
|
||||
grpcMethod: "CreateLead",
|
||||
grpcDescriptorRef: "desc_lead_service",
|
||||
grpcDescriptorSetB64: "",
|
||||
inputSchemaText: defaultInputSchemaText(),
|
||||
outputSchemaText: defaultOutputSchemaText(),
|
||||
inputMappingText: defaultRestInputMappingText(),
|
||||
outputMappingText: defaultRestOutputMappingText(),
|
||||
executionHeadersText: JSON.stringify({}, null, 2),
|
||||
executionConfigText: defaultExecutionConfigText(),
|
||||
};
|
||||
|
||||
export function getProtocolPreset(
|
||||
protocol: OperationFormValues["protocol"],
|
||||
): Partial<OperationFormValues> {
|
||||
switch (protocol) {
|
||||
case "rest":
|
||||
return {
|
||||
name: "crm_create_lead",
|
||||
displayName: "Create Lead",
|
||||
toolTitle: "Create CRM lead",
|
||||
toolDescription: "Creates a CRM lead from MCP input fields.",
|
||||
inputMappingText: defaultRestInputMappingText(),
|
||||
outputMappingText: defaultRestOutputMappingText(),
|
||||
restStaticHeadersText: JSON.stringify({}, null, 2),
|
||||
executionHeadersText: JSON.stringify({}, null, 2),
|
||||
};
|
||||
case "graphql":
|
||||
return {
|
||||
name: "crm_create_lead_graphql",
|
||||
displayName: "Create Lead (GraphQL)",
|
||||
toolTitle: "Create CRM lead through GraphQL",
|
||||
toolDescription:
|
||||
"Executes a fixed GraphQL mutation and returns the selected lead payload.",
|
||||
inputMappingText: defaultGraphqlInputMappingText(),
|
||||
outputMappingText: defaultGraphqlOutputMappingText(),
|
||||
executionHeadersText: JSON.stringify({}, null, 2),
|
||||
};
|
||||
case "grpc":
|
||||
return {
|
||||
name: "crm_create_lead_grpc",
|
||||
displayName: "Create Lead (gRPC)",
|
||||
toolTitle: "Create CRM lead through gRPC",
|
||||
toolDescription:
|
||||
"Executes a unary gRPC method using a descriptor-driven request contract.",
|
||||
inputMappingText: defaultGrpcInputMappingText(),
|
||||
outputMappingText: defaultGrpcOutputMappingText(),
|
||||
executionHeadersText: JSON.stringify({}, null, 2),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function buildOperationPayload(rawValues: OperationFormValues) {
|
||||
const values = operationFormSchema.parse(rawValues);
|
||||
const inputSchema = safeParseJson(values.inputSchemaText, "Input schema");
|
||||
const outputSchema = safeParseJson(values.outputSchemaText, "Output schema");
|
||||
const inputMapping = safeParseJson(values.inputMappingText, "Input mapping");
|
||||
const outputMapping = safeParseJson(values.outputMappingText, "Output mapping");
|
||||
const executionHeaders = parseStringMap(
|
||||
values.executionHeadersText,
|
||||
"Execution headers",
|
||||
);
|
||||
const executionConfig = safeParseJson<Record<string, unknown>>(
|
||||
values.executionConfigText,
|
||||
"Execution config",
|
||||
);
|
||||
const normalizedExecutionConfig = {
|
||||
...executionConfig,
|
||||
headers: executionHeaders,
|
||||
};
|
||||
|
||||
const basePayload = {
|
||||
name: values.name,
|
||||
display_name: values.displayName,
|
||||
input_schema: inputSchema,
|
||||
output_schema: outputSchema,
|
||||
input_mapping: inputMapping,
|
||||
output_mapping: outputMapping,
|
||||
execution_config: normalizedExecutionConfig,
|
||||
tool_description: {
|
||||
title: values.toolTitle,
|
||||
description: values.toolDescription,
|
||||
tags: [values.protocol],
|
||||
examples: [],
|
||||
},
|
||||
};
|
||||
|
||||
switch (values.protocol) {
|
||||
case "rest":
|
||||
return {
|
||||
...basePayload,
|
||||
protocol: "rest",
|
||||
target: {
|
||||
kind: "rest",
|
||||
base_url: values.restBaseUrl,
|
||||
method: values.restMethod,
|
||||
path_template: values.restPathTemplate,
|
||||
static_headers: parseStringMap(
|
||||
values.restStaticHeadersText,
|
||||
"REST static headers",
|
||||
),
|
||||
},
|
||||
};
|
||||
case "graphql":
|
||||
return {
|
||||
...basePayload,
|
||||
protocol: "graphql",
|
||||
target: {
|
||||
kind: "graphql",
|
||||
endpoint: values.graphqlEndpoint,
|
||||
operation_type: values.graphqlOperationType,
|
||||
operation_name: values.graphqlOperationName,
|
||||
query_template: values.graphqlQueryTemplate,
|
||||
response_path: values.graphqlResponsePath,
|
||||
},
|
||||
};
|
||||
case "grpc":
|
||||
return {
|
||||
...basePayload,
|
||||
protocol: "grpc",
|
||||
target: {
|
||||
kind: "grpc",
|
||||
server_addr: values.grpcServerAddr,
|
||||
package: values.grpcPackage,
|
||||
service: values.grpcService,
|
||||
method: values.grpcMethod,
|
||||
descriptor_ref: values.grpcDescriptorRef,
|
||||
descriptor_set_b64: values.grpcDescriptorSetB64,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,540 +0,0 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { createPortal } from "react-dom";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { createOperation } from "../../entities/operation/api";
|
||||
import { ApiError } from "../../shared/api/client";
|
||||
import {
|
||||
buildOperationPayload,
|
||||
defaultOperationFormValues,
|
||||
getProtocolPreset,
|
||||
operationFormSchema,
|
||||
type OperationFormValues,
|
||||
} from "./model";
|
||||
|
||||
type OperationFieldName = keyof OperationFormValues;
|
||||
|
||||
type FieldConfig = {
|
||||
name: OperationFieldName;
|
||||
label: string;
|
||||
description?: string;
|
||||
wide?: boolean;
|
||||
rows?: number;
|
||||
code?: boolean;
|
||||
};
|
||||
|
||||
const protocolCards = [
|
||||
{
|
||||
protocol: "rest",
|
||||
title: "REST / HTTP",
|
||||
description:
|
||||
"One HTTP method and one path template, with body, query and header mapping.",
|
||||
},
|
||||
{
|
||||
protocol: "graphql",
|
||||
title: "GraphQL",
|
||||
description:
|
||||
"One fixed query or mutation with stable variables and a typed response shape.",
|
||||
},
|
||||
{
|
||||
protocol: "grpc",
|
||||
title: "gRPC (unary)",
|
||||
description:
|
||||
"One unary method backed by a descriptor-set contract and typed request payload.",
|
||||
},
|
||||
] as const satisfies Array<{
|
||||
protocol: OperationFormValues["protocol"];
|
||||
title: string;
|
||||
description: string;
|
||||
}>;
|
||||
|
||||
const commonFields: FieldConfig[] = [
|
||||
{ name: "name", label: "Tool name" },
|
||||
{ name: "displayName", label: "Display name" },
|
||||
{ name: "toolTitle", label: "Tool title" },
|
||||
{
|
||||
name: "toolDescription",
|
||||
label: "Description",
|
||||
description: "LLM-facing description for the tool runtime contract.",
|
||||
rows: 4,
|
||||
wide: true,
|
||||
},
|
||||
];
|
||||
|
||||
const restFields: FieldConfig[] = [
|
||||
{ name: "restBaseUrl", label: "Base URL", wide: true },
|
||||
{ name: "restPathTemplate", label: "Path template" },
|
||||
{ name: "restMethod", label: "HTTP method" },
|
||||
];
|
||||
|
||||
const graphqlFields: FieldConfig[] = [
|
||||
{ name: "graphqlEndpoint", label: "GraphQL endpoint", wide: true },
|
||||
{ name: "graphqlOperationType", label: "Operation type" },
|
||||
{ name: "graphqlOperationName", label: "Operation name" },
|
||||
{
|
||||
name: "graphqlResponsePath",
|
||||
label: "Response path",
|
||||
description: "Stable extraction root inside the response payload.",
|
||||
wide: true,
|
||||
},
|
||||
{
|
||||
name: "graphqlQueryTemplate",
|
||||
label: "Query template",
|
||||
description: "Fixed GraphQL document exposed as one MCP tool.",
|
||||
rows: 12,
|
||||
wide: true,
|
||||
code: true,
|
||||
},
|
||||
];
|
||||
|
||||
const grpcFields: FieldConfig[] = [
|
||||
{ name: "grpcServerAddr", label: "Server address", wide: true },
|
||||
{ name: "grpcPackage", label: "Package" },
|
||||
{ name: "grpcService", label: "Service" },
|
||||
{ name: "grpcMethod", label: "Method" },
|
||||
{
|
||||
name: "grpcDescriptorRef",
|
||||
label: "Descriptor reference",
|
||||
description: "Stable descriptor identifier stored with the operation.",
|
||||
wide: true,
|
||||
},
|
||||
{
|
||||
name: "grpcDescriptorSetB64",
|
||||
label: "Descriptor set base64",
|
||||
description: "Compiled descriptor-set contents used for runtime invocation.",
|
||||
rows: 10,
|
||||
wide: true,
|
||||
code: true,
|
||||
},
|
||||
];
|
||||
|
||||
const schemaFields: FieldConfig[] = [
|
||||
{
|
||||
name: "inputSchemaText",
|
||||
label: "Input schema",
|
||||
rows: 14,
|
||||
wide: true,
|
||||
code: true,
|
||||
},
|
||||
{
|
||||
name: "outputSchemaText",
|
||||
label: "Output schema",
|
||||
rows: 14,
|
||||
wide: true,
|
||||
code: true,
|
||||
},
|
||||
];
|
||||
|
||||
const mappingFields: FieldConfig[] = [
|
||||
{
|
||||
name: "inputMappingText",
|
||||
label: "Input → Request mapping",
|
||||
rows: 12,
|
||||
wide: true,
|
||||
code: true,
|
||||
},
|
||||
{
|
||||
name: "outputMappingText",
|
||||
label: "Response → Output mapping",
|
||||
rows: 12,
|
||||
wide: true,
|
||||
code: true,
|
||||
},
|
||||
];
|
||||
|
||||
const headerFields: FieldConfig[] = [
|
||||
{
|
||||
name: "executionHeadersText",
|
||||
label: "Execution headers",
|
||||
description: "Shared transport headers for REST, GraphQL and gRPC requests.",
|
||||
rows: 8,
|
||||
wide: true,
|
||||
code: true,
|
||||
},
|
||||
{
|
||||
name: "executionConfigText",
|
||||
label: "Execution config",
|
||||
description: "Timeouts, auth profile reference and protocol options.",
|
||||
rows: 10,
|
||||
wide: true,
|
||||
code: true,
|
||||
},
|
||||
];
|
||||
|
||||
function protocolSummary(protocol: OperationFormValues["protocol"]) {
|
||||
switch (protocol) {
|
||||
case "rest":
|
||||
return "One tool maps to one HTTP method and one path template.";
|
||||
case "graphql":
|
||||
return "One tool maps to one fixed GraphQL query or mutation.";
|
||||
case "grpc":
|
||||
return "One tool maps to one unary gRPC method backed by a descriptor set.";
|
||||
}
|
||||
}
|
||||
|
||||
function descriptorRefFromFileName(fileName: string) {
|
||||
const normalized = fileName
|
||||
.replace(/\.[^/.]+$/, "")
|
||||
.replace(/[^a-zA-Z0-9]+/g, "_")
|
||||
.replace(/^_+|_+$/g, "")
|
||||
.toLowerCase();
|
||||
|
||||
return normalized.length === 0 ? "desc_uploaded" : `desc_${normalized}`;
|
||||
}
|
||||
|
||||
function bytesToBase64(buffer: ArrayBuffer) {
|
||||
const bytes = new Uint8Array(buffer);
|
||||
let binary = "";
|
||||
|
||||
for (let index = 0; index < bytes.length; index += 0x8000) {
|
||||
binary += String.fromCharCode(...bytes.subarray(index, index + 0x8000));
|
||||
}
|
||||
|
||||
return window.btoa(binary);
|
||||
}
|
||||
|
||||
type OperationFormProps = {
|
||||
actionBarContainerId?: string;
|
||||
};
|
||||
|
||||
export function OperationForm({ actionBarContainerId }: OperationFormProps) {
|
||||
const navigate = useNavigate();
|
||||
const form = useForm<OperationFormValues>({
|
||||
defaultValues: defaultOperationFormValues,
|
||||
resolver: zodResolver(operationFormSchema),
|
||||
});
|
||||
const activeProtocol = form.watch("protocol");
|
||||
const previousProtocolRef = useRef(activeProtocol);
|
||||
|
||||
useEffect(() => {
|
||||
if (previousProtocolRef.current === activeProtocol) {
|
||||
return;
|
||||
}
|
||||
|
||||
const preset = getProtocolPreset(activeProtocol);
|
||||
for (const [fieldName, value] of Object.entries(preset)) {
|
||||
form.setValue(fieldName as OperationFieldName, value);
|
||||
}
|
||||
|
||||
previousProtocolRef.current = activeProtocol;
|
||||
}, [activeProtocol, form]);
|
||||
|
||||
const creationMutation = useMutation({
|
||||
mutationFn: async (values: OperationFormValues) =>
|
||||
createOperation(buildOperationPayload(values)),
|
||||
onSuccess: (data) => {
|
||||
navigate(`/operations/${data.operation_id}`);
|
||||
},
|
||||
});
|
||||
|
||||
const protocolFields = useMemo(() => {
|
||||
switch (activeProtocol) {
|
||||
case "rest":
|
||||
return restFields;
|
||||
case "graphql":
|
||||
return graphqlFields;
|
||||
case "grpc":
|
||||
return grpcFields;
|
||||
}
|
||||
}, [activeProtocol]);
|
||||
|
||||
const actionBarContainer =
|
||||
actionBarContainerId === undefined ? null : document.getElementById(actionBarContainerId);
|
||||
|
||||
async function handleDescriptorFileChange(file: File | undefined) {
|
||||
if (file === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const encoded = bytesToBase64(await file.arrayBuffer());
|
||||
form.setValue("grpcDescriptorSetB64", encoded, {
|
||||
shouldDirty: true,
|
||||
shouldValidate: true,
|
||||
});
|
||||
|
||||
if (form.getValues("grpcDescriptorRef").trim().length === 0) {
|
||||
form.setValue("grpcDescriptorRef", descriptorRefFromFileName(file.name), {
|
||||
shouldDirty: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function fieldError(name: OperationFieldName) {
|
||||
const error = form.formState.errors[name];
|
||||
return typeof error?.message === "string" ? error.message : undefined;
|
||||
}
|
||||
|
||||
function renderSelect(field: FieldConfig) {
|
||||
const message = fieldError(field.name);
|
||||
|
||||
return (
|
||||
<label
|
||||
className={field.wide ? "field-block field-block-wide" : "field-block"}
|
||||
key={field.name}
|
||||
>
|
||||
<span>{field.label}</span>
|
||||
{field.description ? <small className="field-hint">{field.description}</small> : null}
|
||||
<select className="select" {...form.register(field.name)}>
|
||||
{field.name === "restMethod" ? (
|
||||
<>
|
||||
<option value="GET">GET</option>
|
||||
<option value="POST">POST</option>
|
||||
<option value="PUT">PUT</option>
|
||||
<option value="PATCH">PATCH</option>
|
||||
<option value="DELETE">DELETE</option>
|
||||
</>
|
||||
) : null}
|
||||
{field.name === "graphqlOperationType" ? (
|
||||
<>
|
||||
<option value="query">query</option>
|
||||
<option value="mutation">mutation</option>
|
||||
</>
|
||||
) : null}
|
||||
</select>
|
||||
{message ? <small className="field-error">{message}</small> : null}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
function renderField(field: FieldConfig) {
|
||||
if (field.name === "restMethod" || field.name === "graphqlOperationType") {
|
||||
return renderSelect(field);
|
||||
}
|
||||
|
||||
const message = fieldError(field.name);
|
||||
const blockClassName = field.wide ? "field-block field-block-wide" : "field-block";
|
||||
|
||||
if (field.rows !== undefined) {
|
||||
return (
|
||||
<label className={blockClassName} key={field.name}>
|
||||
<span>{field.label}</span>
|
||||
{field.description ? <small className="field-hint">{field.description}</small> : null}
|
||||
{field.code ? (
|
||||
<div className="code-editor-shell">
|
||||
<div className="code-editor-toolbar">
|
||||
<div className="code-editor-dots">
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
</div>
|
||||
<small>json / contract</small>
|
||||
</div>
|
||||
<textarea
|
||||
className="textarea textarea-code"
|
||||
rows={field.rows}
|
||||
{...form.register(field.name)}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<textarea rows={field.rows} {...form.register(field.name)} />
|
||||
)}
|
||||
{message ? <small className="field-error">{message}</small> : null}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<label className={blockClassName} key={field.name}>
|
||||
<span>{field.label}</span>
|
||||
{field.description ? <small className="field-hint">{field.description}</small> : null}
|
||||
<input type="text" className="input" {...form.register(field.name)} />
|
||||
{message ? <small className="field-error">{message}</small> : null}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="builder-stack"
|
||||
id="operation-create-form"
|
||||
onSubmit={form.handleSubmit((values) => {
|
||||
creationMutation.mutate(values);
|
||||
})}
|
||||
>
|
||||
<div className="builder-content">
|
||||
<section className="builder-card">
|
||||
<header className="builder-card-header">
|
||||
<div>
|
||||
<h2>1 — Protocol</h2>
|
||||
<p>One tool maps to exactly one upstream method.</p>
|
||||
</div>
|
||||
</header>
|
||||
<div className="builder-card-body">
|
||||
<div className="proto-grid">
|
||||
{protocolCards.map((card) => {
|
||||
const isSelected = activeProtocol === card.protocol;
|
||||
const selectedClassName = isSelected
|
||||
? `proto-card selected-${card.protocol === "graphql" ? "gql" : card.protocol}`
|
||||
: "proto-card";
|
||||
|
||||
return (
|
||||
<button
|
||||
className={selectedClassName}
|
||||
key={card.protocol}
|
||||
onClick={() => {
|
||||
form.setValue("protocol", card.protocol, {
|
||||
shouldDirty: true,
|
||||
shouldValidate: true,
|
||||
});
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<div className="proto-card-top">
|
||||
<div className={`proto-icon pi-${card.protocol === "graphql" ? "gql" : card.protocol}`}>
|
||||
{card.protocol === "graphql" ? "GQL" : card.protocol.toUpperCase()}
|
||||
</div>
|
||||
<div className="proto-radio" />
|
||||
</div>
|
||||
<div className="proto-card-copy">
|
||||
<h3>{card.title}</h3>
|
||||
<p>{card.description}</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="builder-card">
|
||||
<header className="builder-card-header">
|
||||
<div>
|
||||
<h2>2 — Tool identity</h2>
|
||||
<p>Name and description visible to the LLM at runtime.</p>
|
||||
</div>
|
||||
<span className="status-pill status-draft">draft</span>
|
||||
</header>
|
||||
<div className="builder-card-body">
|
||||
<div className="form-grid">{commonFields.map(renderField)}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="builder-card">
|
||||
<header className="builder-card-header">
|
||||
<div>
|
||||
<h2>3 — Upstream target</h2>
|
||||
<p>Protocol-specific target configuration exposed through one MCP tool.</p>
|
||||
</div>
|
||||
<span className={`protocol-pill protocol-${activeProtocol}`}>{activeProtocol}</span>
|
||||
</header>
|
||||
<div className="builder-card-body">
|
||||
<div className="form-grid">
|
||||
{protocolFields.map(renderField)}
|
||||
{activeProtocol === "grpc" ? (
|
||||
<label className="field-block field-block-wide">
|
||||
<span>Descriptor set file</span>
|
||||
<small className="field-hint">
|
||||
Upload a compiled descriptor-set file to autofill the base64 payload.
|
||||
</small>
|
||||
<input
|
||||
className="input"
|
||||
type="file"
|
||||
accept=".bin,.pb,.desc"
|
||||
onChange={(event) => {
|
||||
void handleDescriptorFileChange(event.target.files?.[0]);
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
) : null}
|
||||
{activeProtocol === "rest"
|
||||
? renderField({
|
||||
name: "restStaticHeadersText",
|
||||
label: "Static headers",
|
||||
description:
|
||||
"Always sent for this REST target before dynamic request headers are merged.",
|
||||
rows: 8,
|
||||
wide: true,
|
||||
code: true,
|
||||
})
|
||||
: null}
|
||||
</div>
|
||||
|
||||
<div className="info-banner">
|
||||
<div className="info-dot" />
|
||||
<p>
|
||||
Headers and auth stay separate from path and payload mapping. Configure shared
|
||||
transport headers in the execution section below.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="builder-card">
|
||||
<header className="builder-card-header">
|
||||
<div>
|
||||
<h2>4 — Contract schemas</h2>
|
||||
<p>Protocol-agnostic schemas for MCP input and output payloads.</p>
|
||||
</div>
|
||||
</header>
|
||||
<div className="builder-card-body">
|
||||
<div className="form-grid">{schemaFields.map(renderField)}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="builder-card">
|
||||
<header className="builder-card-header">
|
||||
<div>
|
||||
<h2>5 — Mapping and execution</h2>
|
||||
<p>
|
||||
Translate MCP input to request fields, then map upstream output back to tool
|
||||
output.
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
<div className="builder-card-body">
|
||||
<div className="section-divider">
|
||||
<span className="section-divider-label">Input → Request</span>
|
||||
<div className="section-divider-line" />
|
||||
</div>
|
||||
<div className="form-grid">{mappingFields.slice(0, 1).map(renderField)}</div>
|
||||
|
||||
<div className="section-divider">
|
||||
<span className="section-divider-label">Response → Output</span>
|
||||
<div className="section-divider-line" />
|
||||
</div>
|
||||
<div className="form-grid">{mappingFields.slice(1).map(renderField)}</div>
|
||||
|
||||
<div className="section-divider">
|
||||
<span className="section-divider-label">Headers And Runtime</span>
|
||||
<div className="section-divider-line" />
|
||||
</div>
|
||||
<div className="form-grid">{headerFields.map(renderField)}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{creationMutation.error ? (
|
||||
<div className="feedback-card feedback-error">
|
||||
{creationMutation.error instanceof ApiError
|
||||
? creationMutation.error.message
|
||||
: "Failed to create operation"}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{actionBarContainer
|
||||
? createPortal(
|
||||
<div className="sticky-action-bar">
|
||||
<div className="sticky-action-copy">
|
||||
<span className="status-pill status-testing">ready to publish later</span>
|
||||
<p>{protocolSummary(activeProtocol)}</p>
|
||||
</div>
|
||||
<button
|
||||
className="button-primary button-primary-strong"
|
||||
type="submit"
|
||||
form="operation-create-form"
|
||||
disabled={creationMutation.isPending}
|
||||
>
|
||||
{creationMutation.isPending ? "Creating..." : "Create operation"}
|
||||
</button>
|
||||
</div>,
|
||||
actionBarContainer,
|
||||
)
|
||||
: null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { publishOperation } from "../../entities/operation/api";
|
||||
import { ApiError } from "../../shared/api/client";
|
||||
|
||||
type PublishOperationPanelProps = {
|
||||
operationId: string;
|
||||
version: number;
|
||||
};
|
||||
|
||||
export function PublishOperationPanel({
|
||||
operationId,
|
||||
version,
|
||||
}: PublishOperationPanelProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const publishMutation = useMutation({
|
||||
mutationFn: async () => publishOperation(operationId, version),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["operations"] });
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["operation-summary", operationId],
|
||||
});
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["operation-version", operationId, version],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="stack-layout">
|
||||
<button
|
||||
className="button-primary"
|
||||
type="button"
|
||||
onClick={() => publishMutation.mutate()}
|
||||
>
|
||||
{publishMutation.isPending ? "Publishing..." : "Publish operation"}
|
||||
</button>
|
||||
{publishMutation.data ? (
|
||||
<div className="feedback-card feedback-success">
|
||||
Published version {String(publishMutation.data.published_version)} at{" "}
|
||||
{String(publishMutation.data.published_at)}
|
||||
</div>
|
||||
) : null}
|
||||
{publishMutation.error ? (
|
||||
<div className="feedback-card feedback-error">
|
||||
{publishMutation.error instanceof ApiError
|
||||
? publishMutation.error.message
|
||||
: "Publish failed"}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
|
||||
import {
|
||||
uploadInputJsonSample,
|
||||
uploadOutputJsonSample,
|
||||
} from "../../entities/operation/api";
|
||||
import { ApiError } from "../../shared/api/client";
|
||||
import { safeParseJson } from "../../shared/lib/json";
|
||||
|
||||
type SampleUploadPanelProps = {
|
||||
operationId: string;
|
||||
};
|
||||
|
||||
export function SampleUploadPanel({ operationId }: SampleUploadPanelProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const [inputSampleText, setInputSampleText] = useState(
|
||||
JSON.stringify({ email: "user@example.com", name: "Ada" }, null, 2),
|
||||
);
|
||||
const [outputSampleText, setOutputSampleText] = useState(
|
||||
JSON.stringify({ id: "lead_123", status: "created" }, null, 2),
|
||||
);
|
||||
|
||||
const createUploadMutation = (
|
||||
mutationKey: "input" | "output",
|
||||
submitFn: (payload: unknown) => Promise<unknown>,
|
||||
) =>
|
||||
useMutation({
|
||||
mutationKey: ["sample-upload", operationId, mutationKey],
|
||||
mutationFn: submitFn,
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["operation-version", operationId],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const inputMutation = createUploadMutation("input", async () =>
|
||||
uploadInputJsonSample(
|
||||
operationId,
|
||||
safeParseJson(inputSampleText, "Input sample"),
|
||||
),
|
||||
);
|
||||
const outputMutation = createUploadMutation("output", async () =>
|
||||
uploadOutputJsonSample(
|
||||
operationId,
|
||||
safeParseJson(outputSampleText, "Output sample"),
|
||||
),
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="split-layout">
|
||||
<div className="stack-layout">
|
||||
<label className="field-block">
|
||||
<span>Input sample</span>
|
||||
<textarea
|
||||
rows={10}
|
||||
value={inputSampleText}
|
||||
onChange={(event) => setInputSampleText(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
className="button-secondary"
|
||||
onClick={() => inputMutation.mutate(undefined)}
|
||||
type="button"
|
||||
>
|
||||
{inputMutation.isPending ? "Uploading..." : "Upload input sample"}
|
||||
</button>
|
||||
{inputMutation.error ? (
|
||||
<small className="field-error">
|
||||
{inputMutation.error instanceof ApiError
|
||||
? inputMutation.error.message
|
||||
: "Failed to upload input sample"}
|
||||
</small>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="stack-layout">
|
||||
<label className="field-block">
|
||||
<span>Output sample</span>
|
||||
<textarea
|
||||
rows={10}
|
||||
value={outputSampleText}
|
||||
onChange={(event) => setOutputSampleText(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
className="button-secondary"
|
||||
onClick={() => outputMutation.mutate(undefined)}
|
||||
type="button"
|
||||
>
|
||||
{outputMutation.isPending ? "Uploading..." : "Upload output sample"}
|
||||
</button>
|
||||
{outputMutation.error ? (
|
||||
<small className="field-error">
|
||||
{outputMutation.error instanceof ApiError
|
||||
? outputMutation.error.message
|
||||
: "Failed to upload output sample"}
|
||||
</small>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import type { DraftGenerationResult, OperationRecord } from "../../entities/operation/types";
|
||||
|
||||
type SchemaViewerPanelProps = {
|
||||
operationRecord?: OperationRecord;
|
||||
draftResult?: DraftGenerationResult;
|
||||
};
|
||||
|
||||
export function SchemaViewerPanel({
|
||||
operationRecord,
|
||||
draftResult,
|
||||
}: SchemaViewerPanelProps) {
|
||||
const inputSchema = draftResult?.input_schema ?? operationRecord?.snapshot.input_schema;
|
||||
const outputSchema =
|
||||
draftResult?.output_schema ?? operationRecord?.snapshot.output_schema;
|
||||
const inputMapping =
|
||||
draftResult?.input_mapping ?? operationRecord?.snapshot.input_mapping;
|
||||
const outputMapping =
|
||||
draftResult?.output_mapping ?? operationRecord?.snapshot.output_mapping;
|
||||
|
||||
return (
|
||||
<div className="result-grid">
|
||||
<div className="result-panel">
|
||||
<h3>Input schema</h3>
|
||||
<pre>{JSON.stringify(inputSchema, null, 2)}</pre>
|
||||
</div>
|
||||
<div className="result-panel">
|
||||
<h3>Output schema</h3>
|
||||
<pre>{JSON.stringify(outputSchema, null, 2)}</pre>
|
||||
</div>
|
||||
<div className="result-panel">
|
||||
<h3>Input mapping</h3>
|
||||
<pre>{JSON.stringify(inputMapping, null, 2)}</pre>
|
||||
</div>
|
||||
<div className="result-panel">
|
||||
<h3>Output mapping</h3>
|
||||
<pre>{JSON.stringify(outputMapping, null, 2)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
|
||||
import { runOperationTest } from "../../entities/operation/api";
|
||||
import { ApiError } from "../../shared/api/client";
|
||||
import { safeParseJson } from "../../shared/lib/json";
|
||||
|
||||
type TestRunPanelProps = {
|
||||
operationId: string;
|
||||
version: number;
|
||||
};
|
||||
|
||||
export function TestRunPanel({ operationId, version }: TestRunPanelProps) {
|
||||
const [inputText, setInputText] = useState(
|
||||
JSON.stringify({ email: "user@example.com" }, null, 2),
|
||||
);
|
||||
|
||||
const testMutation = useMutation({
|
||||
mutationFn: async () =>
|
||||
runOperationTest(
|
||||
operationId,
|
||||
version,
|
||||
safeParseJson(inputText, "Test input"),
|
||||
),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="stack-layout">
|
||||
<label className="field-block">
|
||||
<span>Test input</span>
|
||||
<textarea
|
||||
rows={10}
|
||||
value={inputText}
|
||||
onChange={(event) => setInputText(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
className="button-primary"
|
||||
type="button"
|
||||
onClick={() => testMutation.mutate()}
|
||||
>
|
||||
{testMutation.isPending ? "Running..." : "Run test"}
|
||||
</button>
|
||||
|
||||
{testMutation.error ? (
|
||||
<div className="feedback-card feedback-error">
|
||||
{testMutation.error instanceof ApiError
|
||||
? testMutation.error.message
|
||||
: "Test run failed"}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{testMutation.data ? (
|
||||
<div className="result-grid">
|
||||
<div className="result-panel">
|
||||
<h3>Request preview</h3>
|
||||
<pre>{JSON.stringify(testMutation.data.request_preview, null, 2)}</pre>
|
||||
</div>
|
||||
<div className="result-panel">
|
||||
<h3>Response preview</h3>
|
||||
<pre>{JSON.stringify(testMutation.data.response_preview, null, 2)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
|
||||
import { AppProviders } from "./app/providers";
|
||||
import { AppRouter } from "./app/router";
|
||||
import "./styles.css";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<AppProviders>
|
||||
<AppRouter />
|
||||
</AppProviders>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
@@ -1,5 +0,0 @@
|
||||
import { OperationForm } from "../../features/operation-form/operation-form";
|
||||
|
||||
export function OperationCreatePage() {
|
||||
return <OperationForm actionBarContainerId="app-layout-main" />;
|
||||
}
|
||||
@@ -1,290 +0,0 @@
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
import {
|
||||
exportOperationYaml,
|
||||
generateDraft,
|
||||
getOperation,
|
||||
getOperationVersion,
|
||||
importOperationYaml,
|
||||
listAuthProfiles,
|
||||
} from "../../entities/operation/api";
|
||||
import type { OperationTarget } from "../../entities/operation/types";
|
||||
import { GrpcDescriptorPanel } from "../../features/grpc-descriptor/panel";
|
||||
import { PublishOperationPanel } from "../../features/publish-operation/panel";
|
||||
import { SampleUploadPanel } from "../../features/sample-upload/panel";
|
||||
import { SchemaViewerPanel } from "../../features/schema-viewer/panel";
|
||||
import { TestRunPanel } from "../../features/test-run/panel";
|
||||
import { PageSection } from "../../shared/ui/page-section";
|
||||
|
||||
function describeTarget(target: OperationTarget) {
|
||||
switch (target.kind) {
|
||||
case "rest":
|
||||
return [
|
||||
["Base URL", target.base_url],
|
||||
["Method", target.method],
|
||||
["Path", target.path_template],
|
||||
] as const;
|
||||
case "graphql":
|
||||
return [
|
||||
["Endpoint", target.endpoint],
|
||||
["Operation", `${target.operation_type} ${target.operation_name}`],
|
||||
["Response path", target.response_path],
|
||||
] as const;
|
||||
case "grpc":
|
||||
return [
|
||||
["Server", target.server_addr],
|
||||
["Service", `${target.package}.${target.service}/${target.method}`],
|
||||
["Descriptor ref", target.descriptor_ref],
|
||||
] as const;
|
||||
}
|
||||
}
|
||||
|
||||
function readObjectRecord(value: unknown) {
|
||||
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function readExecutionHeaders(value: unknown) {
|
||||
const config = readObjectRecord(value);
|
||||
const headers = config.headers;
|
||||
|
||||
if (headers === null || typeof headers !== "object" || Array.isArray(headers)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return headers as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function draftSubtitle(target: OperationTarget) {
|
||||
switch (target.kind) {
|
||||
case "rest":
|
||||
return "Upload request and response JSON samples, then infer schema and mapping drafts.";
|
||||
case "graphql":
|
||||
return "Upload stable request and response samples for the fixed GraphQL operation contract.";
|
||||
case "grpc":
|
||||
return "Upload JSON samples or inspect descriptor-driven schemas before refining mappings.";
|
||||
}
|
||||
}
|
||||
|
||||
export function OperationDetailPage() {
|
||||
const { operationId = "" } = useParams();
|
||||
const summaryQuery = useQuery({
|
||||
queryKey: ["operation-summary", operationId],
|
||||
queryFn: () => getOperation(operationId),
|
||||
enabled: operationId.length > 0,
|
||||
});
|
||||
const currentVersion = summaryQuery.data?.current_draft_version ?? 0;
|
||||
const versionQuery = useQuery({
|
||||
queryKey: ["operation-version", operationId, currentVersion],
|
||||
queryFn: () => getOperationVersion(operationId, currentVersion),
|
||||
enabled: operationId.length > 0 && currentVersion > 0,
|
||||
});
|
||||
const authProfilesQuery = useQuery({
|
||||
queryKey: ["auth-profiles"],
|
||||
queryFn: listAuthProfiles,
|
||||
});
|
||||
const draftMutation = useMutation({
|
||||
mutationFn: async () => generateDraft(operationId),
|
||||
});
|
||||
const exportMutation = useMutation({
|
||||
mutationFn: async () => exportOperationYaml(operationId, currentVersion),
|
||||
});
|
||||
const importMutation = useMutation({
|
||||
mutationFn: async (yamlText: string) => importOperationYaml(yamlText, "upsert"),
|
||||
});
|
||||
|
||||
const snapshot = versionQuery.data?.snapshot;
|
||||
const exportedYaml = exportMutation.data;
|
||||
const targetRows = snapshot ? describeTarget(snapshot.target) : [];
|
||||
|
||||
return (
|
||||
<div className="page-stack">
|
||||
<PageSection
|
||||
title={summaryQuery.data?.display_name ?? "Operation"}
|
||||
subtitle={
|
||||
summaryQuery.data
|
||||
? `${summaryQuery.data.name} · ${summaryQuery.data.protocol} · draft v${currentVersion}`
|
||||
: "Loading operation metadata..."
|
||||
}
|
||||
>
|
||||
{summaryQuery.data ? (
|
||||
<div className="detail-hero">
|
||||
<div className="summary-grid">
|
||||
<div className="summary-card">
|
||||
<span>Status</span>
|
||||
<strong>{summaryQuery.data.status}</strong>
|
||||
</div>
|
||||
<div className="summary-card">
|
||||
<span>Published version</span>
|
||||
<strong>{summaryQuery.data.latest_published_version ?? "none"}</strong>
|
||||
</div>
|
||||
<div className="summary-card">
|
||||
<span>Updated</span>
|
||||
<strong>{new Date(summaryQuery.data.updated_at).toLocaleString()}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{snapshot ? (
|
||||
<div className="target-grid">
|
||||
{targetRows.map(([label, value]) => (
|
||||
<div className="target-card" key={label}>
|
||||
<span>{label}</span>
|
||||
<strong>{value}</strong>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</PageSection>
|
||||
|
||||
{snapshot ? (
|
||||
<PageSection
|
||||
title="Target Contract"
|
||||
subtitle="The runtime transport details below are the fixed contract exposed as one MCP tool."
|
||||
>
|
||||
<div className="metadata-grid">
|
||||
<div className="result-panel">
|
||||
<h3>Tool description</h3>
|
||||
<dl className="key-value-list">
|
||||
<div>
|
||||
<dt>Title</dt>
|
||||
<dd>{snapshot.tool_description.title}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Tags</dt>
|
||||
<dd>{snapshot.tool_description.tags.join(", ") || "none"}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<p className="body-copy">{snapshot.tool_description.description}</p>
|
||||
</div>
|
||||
<div className="result-panel">
|
||||
<h3>Execution config</h3>
|
||||
<pre>{JSON.stringify(snapshot.execution_config, null, 2)}</pre>
|
||||
</div>
|
||||
<div className="result-panel">
|
||||
<h3>Transport headers</h3>
|
||||
<pre>{JSON.stringify(readExecutionHeaders(snapshot.execution_config), null, 2)}</pre>
|
||||
</div>
|
||||
{snapshot.target.kind === "rest" ? (
|
||||
<div className="result-panel">
|
||||
<h3>REST static headers</h3>
|
||||
<pre>{JSON.stringify(snapshot.target.static_headers ?? {}, null, 2)}</pre>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</PageSection>
|
||||
) : null}
|
||||
|
||||
{snapshot?.target.kind === "grpc" ? (
|
||||
<PageSection
|
||||
title="gRPC Discovery"
|
||||
subtitle="Descriptor artifacts and discovered unary methods for the current draft version."
|
||||
>
|
||||
<GrpcDescriptorPanel operationId={operationId} version={currentVersion} />
|
||||
</PageSection>
|
||||
) : null}
|
||||
|
||||
<PageSection
|
||||
title="Samples And Draft"
|
||||
subtitle={snapshot ? draftSubtitle(snapshot.target) : "Prepare schema and mapping drafts."}
|
||||
actions={
|
||||
<button
|
||||
className="button-primary"
|
||||
onClick={() => draftMutation.mutate()}
|
||||
type="button"
|
||||
>
|
||||
{draftMutation.isPending ? "Generating..." : "Generate draft"}
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<SampleUploadPanel operationId={operationId} />
|
||||
<SchemaViewerPanel
|
||||
operationRecord={versionQuery.data}
|
||||
draftResult={draftMutation.data}
|
||||
/>
|
||||
</PageSection>
|
||||
|
||||
<div className="detail-columns">
|
||||
<PageSection
|
||||
title="Test Run"
|
||||
subtitle="Run the current draft version through the runtime before publishing."
|
||||
>
|
||||
<TestRunPanel operationId={operationId} version={currentVersion} />
|
||||
</PageSection>
|
||||
|
||||
<PageSection
|
||||
title="Publish"
|
||||
subtitle="Promote the current draft version into the active MCP tool catalog."
|
||||
>
|
||||
<PublishOperationPanel operationId={operationId} version={currentVersion} />
|
||||
</PageSection>
|
||||
</div>
|
||||
|
||||
<div className="detail-columns">
|
||||
<PageSection
|
||||
title="YAML Export And Reimport"
|
||||
subtitle="Export the current version, inspect the YAML, then upsert it through the same backend contract."
|
||||
actions={
|
||||
<button
|
||||
className="button-secondary"
|
||||
type="button"
|
||||
onClick={() => exportMutation.mutate()}
|
||||
>
|
||||
{exportMutation.isPending ? "Exporting..." : "Export YAML"}
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<div className="stack-layout">
|
||||
{exportedYaml ? (
|
||||
<>
|
||||
<pre className="yaml-panel">{exportedYaml}</pre>
|
||||
<div className="button-row">
|
||||
<button
|
||||
className="button-primary"
|
||||
type="button"
|
||||
onClick={() => importMutation.mutate(exportedYaml)}
|
||||
>
|
||||
{importMutation.isPending ? "Importing..." : "Upsert from YAML"}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p className="body-copy">
|
||||
Export YAML to inspect the current operation snapshot and transport contract.
|
||||
</p>
|
||||
)}
|
||||
{importMutation.data ? (
|
||||
<div className="feedback-card feedback-success">
|
||||
Imported version {String(importMutation.data.version)} in{" "}
|
||||
{String(importMutation.data.import_mode)} mode.
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</PageSection>
|
||||
|
||||
<PageSection
|
||||
title="Auth Profiles"
|
||||
subtitle="Profiles currently available to the admin backend and execution config."
|
||||
>
|
||||
<div className="operation-grid">
|
||||
{(authProfilesQuery.data?.items ?? []).map((profile) => (
|
||||
<div className="operation-card" key={profile.id}>
|
||||
<div className="operation-card-top">
|
||||
<span className="status-pill status-testing">{profile.kind}</span>
|
||||
</div>
|
||||
<h3>{profile.name}</h3>
|
||||
<p className="operation-name">{profile.id}</p>
|
||||
<pre>{JSON.stringify(profile.config, null, 2)}</pre>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</PageSection>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
import { useDeferredValue, useMemo, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { listOperations } from "../../entities/operation/api";
|
||||
import { PageSection } from "../../shared/ui/page-section";
|
||||
|
||||
export function OperationListPage() {
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const deferredSearchTerm = useDeferredValue(searchTerm);
|
||||
const operationsQuery = useQuery({
|
||||
queryKey: ["operations"],
|
||||
queryFn: listOperations,
|
||||
});
|
||||
|
||||
const filteredItems = useMemo(() => {
|
||||
const items = operationsQuery.data?.items ?? [];
|
||||
const normalizedSearchTerm = deferredSearchTerm.trim().toLowerCase();
|
||||
|
||||
if (!normalizedSearchTerm) {
|
||||
return items;
|
||||
}
|
||||
|
||||
return items.filter((item) =>
|
||||
`${item.name} ${item.display_name} ${item.protocol} ${item.status}`
|
||||
.toLowerCase()
|
||||
.includes(normalizedSearchTerm),
|
||||
);
|
||||
}, [deferredSearchTerm, operationsQuery.data?.items]);
|
||||
|
||||
const metrics = useMemo(() => {
|
||||
const items = operationsQuery.data?.items ?? [];
|
||||
|
||||
return {
|
||||
total: items.length,
|
||||
published: items.filter((item) => item.status === "published").length,
|
||||
drafts: items.filter((item) => item.status === "draft").length,
|
||||
protocols: Array.from(new Set(items.map((item) => item.protocol))).length,
|
||||
};
|
||||
}, [operationsQuery.data?.items]);
|
||||
|
||||
return (
|
||||
<div className="page-stack">
|
||||
<PageSection
|
||||
title="Operations"
|
||||
subtitle="Browse draft and published MCP tools exposed through the admin backend."
|
||||
actions={
|
||||
<Link className="button-primary" to="/operations/new">
|
||||
New operation
|
||||
</Link>
|
||||
}
|
||||
>
|
||||
<div className="hero-grid">
|
||||
<div className="hero-card hero-card-accent">
|
||||
<p className="eyebrow">Catalog health</p>
|
||||
<h3>{metrics.total} tools registered</h3>
|
||||
<p className="body-copy">
|
||||
Published and draft operations share one operator surface and one
|
||||
MCP publication path.
|
||||
</p>
|
||||
</div>
|
||||
<div className="hero-card">
|
||||
<div className="hero-metrics">
|
||||
<div>
|
||||
<span>Published</span>
|
||||
<strong>{metrics.published}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Drafts</span>
|
||||
<strong>{metrics.drafts}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Protocols</span>
|
||||
<strong>{metrics.protocols}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="toolbar-row">
|
||||
<label className="field-block field-inline">
|
||||
<span>Search</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="name, protocol or status"
|
||||
value={searchTerm}
|
||||
onChange={(event) => setSearchTerm(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className="toolbar-meta">
|
||||
<span className="workspace-badge">{filteredItems.length} visible</span>
|
||||
<span className="workspace-badge">
|
||||
{operationsQuery.data?.items.length ?? 0} total
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{operationsQuery.isLoading ? (
|
||||
<div className="empty-state">
|
||||
<h3>Loading operations</h3>
|
||||
<p>Fetching the current registry snapshot from the admin backend.</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{operationsQuery.data && filteredItems.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
<h3>No matching operations</h3>
|
||||
<p>
|
||||
Adjust the search term or create a new contract for REST, GraphQL
|
||||
or unary gRPC.
|
||||
</p>
|
||||
<Link className="button-secondary" to="/operations/new">
|
||||
Create operation
|
||||
</Link>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{operationsQuery.data && filteredItems.length > 0 ? (
|
||||
<div className="operation-grid">
|
||||
{filteredItems.map((item) => (
|
||||
<Link className="operation-card" key={item.id} to={`/operations/${item.id}`}>
|
||||
<div className="operation-card-top">
|
||||
<span className={`protocol-pill protocol-${item.protocol}`}>
|
||||
{item.protocol}
|
||||
</span>
|
||||
<span className={`status-pill status-${item.status}`}>
|
||||
{item.status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="operation-card-header">
|
||||
<h3>{item.display_name}</h3>
|
||||
<p className="operation-name">{item.name}</p>
|
||||
</div>
|
||||
<dl className="key-value-list">
|
||||
<div>
|
||||
<dt>Draft</dt>
|
||||
<dd>{item.current_draft_version}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Published</dt>
|
||||
<dd>{item.latest_published_version ?? "none"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Updated</dt>
|
||||
<dd>{new Date(item.updated_at).toLocaleString()}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div className="card-link-row">
|
||||
<span>Open workspace</span>
|
||||
<strong>→</strong>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</PageSection>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
export class ApiError extends Error {
|
||||
readonly status: number;
|
||||
|
||||
constructor(message: string, status: number) {
|
||||
super(message);
|
||||
this.name = "ApiError";
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
type RequestInitWithJson = RequestInit & {
|
||||
json?: unknown;
|
||||
};
|
||||
|
||||
async function request<TResponse>(
|
||||
input: string,
|
||||
init: RequestInitWithJson = {},
|
||||
): Promise<TResponse> {
|
||||
const headers = new Headers(init.headers);
|
||||
const body =
|
||||
init.json === undefined ? init.body : JSON.stringify(init.json, null, 2);
|
||||
|
||||
if (init.json !== undefined) {
|
||||
headers.set("Content-Type", "application/json");
|
||||
}
|
||||
|
||||
const response = await fetch(input, {
|
||||
...init,
|
||||
headers,
|
||||
body,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let message = `${response.status} ${response.statusText}`;
|
||||
|
||||
try {
|
||||
const errorPayload = (await response.json()) as {
|
||||
error?: { message?: string };
|
||||
};
|
||||
message = errorPayload.error?.message ?? message;
|
||||
} catch {
|
||||
message = `${response.status} ${response.statusText}`;
|
||||
}
|
||||
|
||||
throw new ApiError(message, response.status);
|
||||
}
|
||||
|
||||
const contentType = response.headers.get("Content-Type") ?? "";
|
||||
if (contentType.includes("application/yaml")) {
|
||||
return (await response.text()) as TResponse;
|
||||
}
|
||||
|
||||
return (await response.json()) as TResponse;
|
||||
}
|
||||
|
||||
export function getJson<TResponse>(input: string) {
|
||||
return request<TResponse>(input);
|
||||
}
|
||||
|
||||
export function getText(input: string) {
|
||||
return request<string>(input);
|
||||
}
|
||||
|
||||
export function postJson<TResponse>(input: string, json?: unknown) {
|
||||
return request<TResponse>(input, {
|
||||
method: "POST",
|
||||
json,
|
||||
});
|
||||
}
|
||||
|
||||
export function postText<TResponse>(input: string, text: string) {
|
||||
return request<TResponse>(input, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/yaml",
|
||||
},
|
||||
body: text,
|
||||
});
|
||||
}
|
||||
|
||||
export function postBytes<TResponse>(
|
||||
input: string,
|
||||
bytes: ArrayBuffer,
|
||||
headers?: Record<string, string>,
|
||||
) {
|
||||
return request<TResponse>(input, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: bytes,
|
||||
});
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
export function safeParseJson<TValue>(raw: string, fieldName: string): TValue {
|
||||
try {
|
||||
return JSON.parse(raw) as TValue;
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`${fieldName} contains invalid JSON: ${error instanceof Error ? error.message : "unknown error"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function prettyJson(value: unknown): string {
|
||||
return JSON.stringify(value, null, 2);
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
import { ReactNode } from "react";
|
||||
import { NavLink, useLocation } from "react-router-dom";
|
||||
|
||||
type AppShellProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
function routeMeta(pathname: string) {
|
||||
if (pathname === "/operations/new") {
|
||||
return {
|
||||
section: "Operations",
|
||||
title: "New Operation",
|
||||
heading: "Create tool contract",
|
||||
subtitle:
|
||||
"Define a single upstream endpoint and expose it as one MCP tool. Choose a protocol, fill the contract and map fields.",
|
||||
};
|
||||
}
|
||||
|
||||
if (pathname.startsWith("/operations/")) {
|
||||
return {
|
||||
section: "Operations",
|
||||
title: "Operation Workspace",
|
||||
heading: "Refine operation contract",
|
||||
subtitle:
|
||||
"Inspect target metadata, validate mappings, run tests and publish the active draft version.",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
section: "Operations",
|
||||
title: "Catalog",
|
||||
heading: "Operation catalog",
|
||||
subtitle:
|
||||
"Track draft and published tool contracts across REST, GraphQL and unary gRPC.",
|
||||
};
|
||||
}
|
||||
|
||||
export function AppShell({ children }: AppShellProps) {
|
||||
const location = useLocation();
|
||||
const meta = routeMeta(location.pathname);
|
||||
|
||||
return (
|
||||
<div className="layout-shell">
|
||||
<aside className="layout-sidebar">
|
||||
<div className="sidebar-logo">
|
||||
<div className="logo-mark">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4">
|
||||
<path d="M12 2 2 7l10 5 10-5-10-5Z" />
|
||||
<path d="m2 12 10 5 10-5" />
|
||||
<path d="m2 17 10 5 10-5" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="logo-text">
|
||||
<strong>Crank</strong>
|
||||
<span>Tool Console</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="sidebar-section">
|
||||
<p className="sidebar-label">Workspace</p>
|
||||
<nav className="sidebar-nav">
|
||||
<NavLink
|
||||
end
|
||||
to="/operations"
|
||||
className={({ isActive }) =>
|
||||
isActive ? "nav-link nav-link-active" : "nav-link"
|
||||
}
|
||||
>
|
||||
<div className="nav-copy">
|
||||
<strong>Operations</strong>
|
||||
<span>Catalog, status and search across all tools.</span>
|
||||
</div>
|
||||
<span className="nav-badge">live</span>
|
||||
</NavLink>
|
||||
|
||||
<NavLink
|
||||
to="/operations/new"
|
||||
className={({ isActive }) =>
|
||||
isActive ? "nav-link nav-link-active" : "nav-link"
|
||||
}
|
||||
>
|
||||
<div className="nav-copy">
|
||||
<strong>New Operation</strong>
|
||||
<span>Create a new protocol-specific tool contract.</span>
|
||||
</div>
|
||||
</NavLink>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div className="sidebar-divider" />
|
||||
|
||||
<div className="sidebar-section">
|
||||
<p className="sidebar-label">Protocols</p>
|
||||
<div className="sidebar-pill-row">
|
||||
<span className="protocol-pill protocol-rest">REST</span>
|
||||
<span className="protocol-pill protocol-graphql">GraphQL</span>
|
||||
<span className="protocol-pill protocol-grpc">gRPC</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="sidebar-footer">
|
||||
<div className="sidebar-user-avatar" />
|
||||
<div className="sidebar-user-copy">
|
||||
<strong>Operator</strong>
|
||||
<span>Admin</span>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div className="layout-main" id="app-layout-main">
|
||||
<header className="topbar">
|
||||
<div className="breadcrumb">
|
||||
<span>{meta.section}</span>
|
||||
<span className="breadcrumb-separator">/</span>
|
||||
<strong>{meta.title}</strong>
|
||||
</div>
|
||||
<div className="topbar-actions">
|
||||
<span className="workspace-badge">MCP request-response</span>
|
||||
<span className="workspace-badge">JSONPath mappings</span>
|
||||
<span className="workspace-badge">YAML import/export</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="main-content" id="app-main-content">
|
||||
<div className="main-content-inner">
|
||||
<div className="page-header">
|
||||
<h1>{meta.heading}</h1>
|
||||
<p>{meta.subtitle}</p>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
type PageSectionProps = {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
actions?: ReactNode;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export function PageSection({
|
||||
title,
|
||||
subtitle,
|
||||
actions,
|
||||
children,
|
||||
}: PageSectionProps) {
|
||||
return (
|
||||
<section className="page-section">
|
||||
<header className="section-header">
|
||||
<div>
|
||||
<h2>{title}</h2>
|
||||
{subtitle ? <p>{subtitle}</p> : null}
|
||||
</div>
|
||||
{actions ? <div className="section-actions">{actions}</div> : null}
|
||||
</header>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
port: 3000,
|
||||
proxy: {
|
||||
"/api/admin": {
|
||||
target: "http://127.0.0.1:3001",
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
test: {
|
||||
environment: "jsdom",
|
||||
},
|
||||
});
|
||||
+148
-422
@@ -2,16 +2,15 @@
|
||||
|
||||
## 1. Назначение документа
|
||||
|
||||
Этот документ фиксирует HTTP-контракты административного API, через которое UI управляет операциями, загружает артефакты, тестирует вызовы и выполняет YAML import/export.
|
||||
|
||||
Документ задает логический контракт. Конкретные детали `axum` handlers, auth middleware и response envelope могут уточняться при реализации.
|
||||
Этот документ фиксирует целевые HTTP-контракты административного API, через которое UI управляет workspace, operations, agents, platform access и observability.
|
||||
|
||||
## 2. Общие правила API
|
||||
|
||||
- все payload по умолчанию в `JSON`;
|
||||
- import/export конфигурации используют `YAML` как payload или файл;
|
||||
- версии operation адресуются явно;
|
||||
- published операция - это ссылка на конкретную version;
|
||||
- import/export конфигурации используют `YAML`;
|
||||
- все основные ресурсы являются `workspace-scoped`;
|
||||
- версии operation и agent адресуются явно;
|
||||
- published operation и published agent - ссылки на конкретные version;
|
||||
- ошибки валидации возвращаются отдельно от transport errors.
|
||||
|
||||
Базовый префикс:
|
||||
@@ -22,431 +21,158 @@
|
||||
|
||||
## 3. Основные ресурсы
|
||||
|
||||
- `workspaces`
|
||||
- `memberships`
|
||||
- `invitations`
|
||||
- `operations`
|
||||
- `versions`
|
||||
- `auth-profiles`
|
||||
- `agents`
|
||||
- `platform-api-keys`
|
||||
- `logs`
|
||||
- `usage`
|
||||
- `samples`
|
||||
- `descriptors`
|
||||
- `auth-profiles`
|
||||
- `test-runs`
|
||||
- `config import/export`
|
||||
|
||||
## 4. CRUD операций
|
||||
## 4. Workspace-scoped routing
|
||||
|
||||
### `GET /api/admin/operations`
|
||||
Канонический префикс для UI-driven сценариев:
|
||||
|
||||
Назначение:
|
||||
|
||||
- список операций для UI.
|
||||
|
||||
Параметры:
|
||||
|
||||
- `protocol`
|
||||
- `status`
|
||||
- `search`
|
||||
|
||||
Ответ:
|
||||
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"id": "op_01",
|
||||
"name": "crm_create_lead",
|
||||
"display_name": "Create Lead",
|
||||
"protocol": "rest",
|
||||
"status": "draft",
|
||||
"current_draft_version": 3,
|
||||
"latest_published_version": 2,
|
||||
"updated_at": "2026-03-25T09:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```text
|
||||
/api/admin/workspaces/{workspace_id}
|
||||
```
|
||||
|
||||
### `POST /api/admin/operations`
|
||||
|
||||
Назначение:
|
||||
|
||||
- создание новой операции и версии `1`.
|
||||
|
||||
Тело:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "crm_create_lead",
|
||||
"display_name": "Create Lead",
|
||||
"protocol": "rest",
|
||||
"target": {
|
||||
"kind": "rest",
|
||||
"base_url": "https://api.example.com",
|
||||
"method": "POST",
|
||||
"path_template": "/v1/leads"
|
||||
},
|
||||
"input_schema": { "type": "object", "fields": {} },
|
||||
"output_schema": { "type": "object", "fields": {} },
|
||||
"input_mapping": { "rules": [] },
|
||||
"output_mapping": { "rules": [] },
|
||||
"execution_config": {
|
||||
"timeout_ms": 10000
|
||||
},
|
||||
"tool_description": {
|
||||
"title": "Create CRM lead",
|
||||
"description": "Creates a new lead."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Ответ:
|
||||
|
||||
```json
|
||||
{
|
||||
"operation_id": "op_01",
|
||||
"version": 1,
|
||||
"status": "draft"
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /api/admin/operations/{operation_id}`
|
||||
|
||||
Назначение:
|
||||
|
||||
- получить метаданные operation и ссылки на draft/published версии.
|
||||
|
||||
### `GET /api/admin/operations/{operation_id}/versions/{version}`
|
||||
|
||||
Назначение:
|
||||
|
||||
- получить полную конфигурацию конкретной версии.
|
||||
|
||||
### `POST /api/admin/operations/{operation_id}/versions`
|
||||
|
||||
Назначение:
|
||||
|
||||
- создать новую draft-версию на основе текущего payload.
|
||||
|
||||
Тело:
|
||||
|
||||
- полная конфигурация operation;
|
||||
- опционально `change_note`.
|
||||
|
||||
Ответ:
|
||||
|
||||
```json
|
||||
{
|
||||
"operation_id": "op_01",
|
||||
"version": 4,
|
||||
"status": "draft"
|
||||
}
|
||||
```
|
||||
|
||||
## 5. Публикация
|
||||
|
||||
### `POST /api/admin/operations/{operation_id}/publish`
|
||||
|
||||
Назначение:
|
||||
|
||||
- опубликовать текущую draft-версию.
|
||||
|
||||
Тело:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 4
|
||||
}
|
||||
```
|
||||
|
||||
Ответ:
|
||||
|
||||
```json
|
||||
{
|
||||
"operation_id": "op_01",
|
||||
"published_version": 4,
|
||||
"published_at": "2026-03-25T10:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### `POST /api/admin/operations/{operation_id}/archive`
|
||||
|
||||
Назначение:
|
||||
|
||||
- перевести operation в archived status.
|
||||
|
||||
## 6. Samples и schema artifacts
|
||||
|
||||
### `POST /api/admin/operations/{operation_id}/samples/input-json`
|
||||
|
||||
Назначение:
|
||||
|
||||
- загрузить sample входного JSON.
|
||||
|
||||
Тип:
|
||||
|
||||
- `multipart/form-data` или raw `application/json`.
|
||||
|
||||
Ответ:
|
||||
|
||||
```json
|
||||
{
|
||||
"sample_id": "file_01",
|
||||
"sample_kind": "input_json"
|
||||
}
|
||||
```
|
||||
|
||||
### `POST /api/admin/operations/{operation_id}/samples/output-json`
|
||||
|
||||
Назначение:
|
||||
|
||||
- загрузить sample выходного JSON.
|
||||
|
||||
`admin-api v1` реализует именно JSON samples, потому что они нужны для REST сценария и draft generation уже на первом этапе.
|
||||
|
||||
### `POST /api/admin/operations/{operation_id}/drafts/generate`
|
||||
|
||||
Назначение:
|
||||
|
||||
- построить черновую схему и mappings из сохраненных JSON samples.
|
||||
|
||||
В `admin-api v1` endpoint возвращает:
|
||||
|
||||
- `generated_draft`
|
||||
- сгенерированные `input_schema`
|
||||
- сгенерированные `output_schema`
|
||||
- сгенерированные `input_mapping`
|
||||
- сгенерированные `output_mapping`
|
||||
|
||||
## 7. gRPC descriptor endpoints
|
||||
|
||||
Следующие endpoints относятся к фазе `grpc-support` и реализованы как часть gRPC vertical slice:
|
||||
|
||||
### `POST /api/admin/operations/{operation_id}/descriptors/proto`
|
||||
|
||||
Назначение:
|
||||
|
||||
- загрузить `.proto`.
|
||||
|
||||
### `POST /api/admin/operations/{operation_id}/descriptors/descriptor-set`
|
||||
|
||||
Назначение:
|
||||
|
||||
- загрузить `descriptor set`.
|
||||
|
||||
Ответ:
|
||||
|
||||
```json
|
||||
{
|
||||
"descriptor_id": "desc_01",
|
||||
"version": 1
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /api/admin/operations/{operation_id}/grpc/services`
|
||||
|
||||
Назначение:
|
||||
|
||||
- получить discovery summary по services и methods.
|
||||
|
||||
Ответ:
|
||||
|
||||
```json
|
||||
{
|
||||
"services": [
|
||||
{
|
||||
"package": "crm.v1",
|
||||
"service": "LeadService",
|
||||
"methods": [
|
||||
{
|
||||
"name": "CreateLead",
|
||||
"kind": "unary",
|
||||
"input_schema": { "type": "object", "fields": {} },
|
||||
"output_schema": { "type": "object", "fields": {} }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Discovery endpoint используется для выбора unary метода и для построения UI-формы входа/выхода до публикации операции.
|
||||
|
||||
## 8. Тестовый запуск
|
||||
|
||||
### `POST /api/admin/operations/{operation_id}/test-runs`
|
||||
|
||||
Назначение:
|
||||
|
||||
- выполнить тестовый вызов draft-конфигурации.
|
||||
|
||||
Тело:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 4,
|
||||
"input": {
|
||||
"name": "Alice",
|
||||
"email": "alice@example.com"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Ответ:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"request_preview": {
|
||||
"body": {
|
||||
"name": "Alice",
|
||||
"email": "alice@example.com"
|
||||
}
|
||||
},
|
||||
"response_preview": {
|
||||
"id": "lead_123",
|
||||
"status": "created"
|
||||
},
|
||||
"errors": []
|
||||
}
|
||||
```
|
||||
|
||||
## 9. Auth profiles
|
||||
|
||||
### `GET /api/admin/auth-profiles`
|
||||
|
||||
Назначение:
|
||||
|
||||
- список доступных профилей аутентификации.
|
||||
|
||||
### `POST /api/admin/auth-profiles`
|
||||
|
||||
Назначение:
|
||||
|
||||
- создать новый auth profile.
|
||||
|
||||
Тело:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "crm-prod-bearer",
|
||||
"kind": "bearer",
|
||||
"config": {
|
||||
"header_name": "Authorization",
|
||||
"secret_ref": "secret://auth/crm-prod-token"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /api/admin/auth-profiles/{auth_profile_id}`
|
||||
|
||||
Назначение:
|
||||
|
||||
- получить metadata auth profile без раскрытия секрета.
|
||||
|
||||
## 10. YAML export
|
||||
|
||||
### `GET /api/admin/operations/{operation_id}/export`
|
||||
|
||||
Назначение:
|
||||
|
||||
- экспортировать конфигурацию операции в `YAML`.
|
||||
|
||||
Параметры:
|
||||
|
||||
- `version` - опционально, если нужно экспортировать не current draft;
|
||||
- `mode=portable|bundle`
|
||||
|
||||
Ответ:
|
||||
|
||||
- `Content-Type: application/yaml`
|
||||
- тело ответа - YAML document
|
||||
|
||||
### Пример YAML response
|
||||
|
||||
```yaml
|
||||
format_version: "1"
|
||||
kind: operation
|
||||
operation:
|
||||
name: crm_create_lead
|
||||
protocol: rest
|
||||
status: draft
|
||||
```
|
||||
|
||||
## 11. YAML import
|
||||
|
||||
### `POST /api/admin/operations/import`
|
||||
|
||||
Назначение:
|
||||
|
||||
- импортировать operation из YAML.
|
||||
|
||||
Тип:
|
||||
|
||||
- `application/yaml`
|
||||
- или `multipart/form-data` с YAML файлом
|
||||
|
||||
Параметры:
|
||||
|
||||
- `mode=create|upsert`
|
||||
|
||||
Ответ:
|
||||
|
||||
```json
|
||||
{
|
||||
"operation_id": "op_01",
|
||||
"version": 5,
|
||||
"import_mode": "upsert",
|
||||
"warnings": []
|
||||
}
|
||||
```
|
||||
|
||||
## 12. Ошибки
|
||||
|
||||
Рекомендуемые классы ошибок:
|
||||
|
||||
- `validation_error`
|
||||
- `mapping_error`
|
||||
- `schema_error`
|
||||
- `descriptor_error`
|
||||
- `auth_profile_error`
|
||||
- `yaml_import_error`
|
||||
- `runtime_test_error`
|
||||
- `not_found`
|
||||
- `conflict`
|
||||
|
||||
Пример:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "validation_error",
|
||||
"message": "Invalid JSONPath in input_mapping rule 2",
|
||||
"details": {
|
||||
"field": "input_mapping.rules[1].source"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 13. Что важно не допустить
|
||||
|
||||
- смешивание CRUD и publish semantics в одном endpoint;
|
||||
- обновление draft "поверх" существующей версии без создания новой version;
|
||||
- YAML import как скрытый апдейт без явного режима `create|upsert`;
|
||||
- возврат открытых секретов из auth-profile endpoints;
|
||||
- привязку runtime к admin DTO;
|
||||
- endpoints, возвращающие разные формы одной и той же сущности без причины.
|
||||
|
||||
## 14. Практический итог
|
||||
|
||||
Минимальный рабочий набор admin API для MVP:
|
||||
|
||||
- список и чтение operations;
|
||||
- создание новой version;
|
||||
- publish;
|
||||
- upload JSON samples;
|
||||
- auth profiles;
|
||||
- generate draft;
|
||||
## 5. Группы endpoints
|
||||
|
||||
### 5.1. Workspaces and members
|
||||
|
||||
- `GET /api/admin/workspaces`
|
||||
- `POST /api/admin/workspaces`
|
||||
- `GET /api/admin/workspaces/{workspace_id}`
|
||||
- `PATCH /api/admin/workspaces/{workspace_id}`
|
||||
- `GET /api/admin/workspaces/{workspace_id}/members`
|
||||
- `POST /api/admin/workspaces/{workspace_id}/invitations`
|
||||
- `DELETE /api/admin/workspaces/{workspace_id}/invitations/{invitation_id}`
|
||||
|
||||
### 5.2. Operations
|
||||
|
||||
- `GET /api/admin/workspaces/{workspace_id}/operations`
|
||||
- `POST /api/admin/workspaces/{workspace_id}/operations`
|
||||
- `GET /api/admin/workspaces/{workspace_id}/operations/{operation_id}`
|
||||
- `PATCH /api/admin/workspaces/{workspace_id}/operations/{operation_id}`
|
||||
- `DELETE /api/admin/workspaces/{workspace_id}/operations/{operation_id}`
|
||||
- `POST /api/admin/workspaces/{workspace_id}/operations/{operation_id}/versions`
|
||||
- `GET /api/admin/workspaces/{workspace_id}/operations/{operation_id}/versions/{version}`
|
||||
- `POST /api/admin/workspaces/{workspace_id}/operations/{operation_id}/publish`
|
||||
- `POST /api/admin/workspaces/{workspace_id}/operations/{operation_id}/archive`
|
||||
- `POST /api/admin/workspaces/{workspace_id}/operations/{operation_id}/test-runs`
|
||||
- `GET /api/admin/workspaces/{workspace_id}/operations/{operation_id}/export`
|
||||
- `POST /api/admin/workspaces/{workspace_id}/operations/import`
|
||||
|
||||
### 5.3. Samples and descriptors
|
||||
|
||||
- `POST /api/admin/workspaces/{workspace_id}/operations/{operation_id}/samples/input-json`
|
||||
- `POST /api/admin/workspaces/{workspace_id}/operations/{operation_id}/samples/output-json`
|
||||
- `POST /api/admin/workspaces/{workspace_id}/operations/{operation_id}/drafts/generate`
|
||||
- `POST /api/admin/workspaces/{workspace_id}/operations/{operation_id}/descriptors/proto`
|
||||
- `POST /api/admin/workspaces/{workspace_id}/operations/{operation_id}/descriptors/descriptor-set`
|
||||
- `GET /api/admin/workspaces/{workspace_id}/operations/{operation_id}/grpc/services`
|
||||
|
||||
### 5.4. Upstream auth profiles
|
||||
|
||||
- `GET /api/admin/workspaces/{workspace_id}/auth-profiles`
|
||||
- `POST /api/admin/workspaces/{workspace_id}/auth-profiles`
|
||||
- `GET /api/admin/workspaces/{workspace_id}/auth-profiles/{auth_profile_id}`
|
||||
- `PATCH /api/admin/workspaces/{workspace_id}/auth-profiles/{auth_profile_id}`
|
||||
- `DELETE /api/admin/workspaces/{workspace_id}/auth-profiles/{auth_profile_id}`
|
||||
|
||||
### 5.5. Agents
|
||||
|
||||
- `GET /api/admin/workspaces/{workspace_id}/agents`
|
||||
- `POST /api/admin/workspaces/{workspace_id}/agents`
|
||||
- `GET /api/admin/workspaces/{workspace_id}/agents/{agent_id}`
|
||||
- `PATCH /api/admin/workspaces/{workspace_id}/agents/{agent_id}`
|
||||
- `DELETE /api/admin/workspaces/{workspace_id}/agents/{agent_id}`
|
||||
- `POST /api/admin/workspaces/{workspace_id}/agents/{agent_id}/versions`
|
||||
- `GET /api/admin/workspaces/{workspace_id}/agents/{agent_id}/versions/{version}`
|
||||
- `POST /api/admin/workspaces/{workspace_id}/agents/{agent_id}/publish`
|
||||
- `POST /api/admin/workspaces/{workspace_id}/agents/{agent_id}/bindings`
|
||||
- `DELETE /api/admin/workspaces/{workspace_id}/agents/{agent_id}/bindings/{operation_id}`
|
||||
|
||||
### 5.6. Platform API keys
|
||||
|
||||
- `GET /api/admin/workspaces/{workspace_id}/platform-api-keys`
|
||||
- `POST /api/admin/workspaces/{workspace_id}/platform-api-keys`
|
||||
- `POST /api/admin/workspaces/{workspace_id}/platform-api-keys/{key_id}/revoke`
|
||||
- `DELETE /api/admin/workspaces/{workspace_id}/platform-api-keys/{key_id}`
|
||||
|
||||
### 5.7. Observability
|
||||
|
||||
- `GET /api/admin/workspaces/{workspace_id}/logs`
|
||||
- `GET /api/admin/workspaces/{workspace_id}/logs/{log_id}`
|
||||
- `GET /api/admin/workspaces/{workspace_id}/usage`
|
||||
- `GET /api/admin/workspaces/{workspace_id}/usage/operations/{operation_id}`
|
||||
- `GET /api/admin/workspaces/{workspace_id}/usage/agents/{agent_id}`
|
||||
|
||||
## 6. Page-to-endpoint mapping
|
||||
|
||||
### Operations catalog
|
||||
|
||||
Нужны:
|
||||
|
||||
- список операций;
|
||||
- удаление операции;
|
||||
- edit/open operation;
|
||||
- publish/archive;
|
||||
- usage summary для карточек и фильтров.
|
||||
|
||||
### Wizard
|
||||
|
||||
Нужны:
|
||||
|
||||
- create/update version;
|
||||
- test run;
|
||||
- YAML import/export.
|
||||
- samples;
|
||||
- draft generation;
|
||||
- gRPC descriptor upload и discovery.
|
||||
|
||||
Этого достаточно, чтобы UI полностью управлял жизненным циклом operation без ручного редактирования кода backend.
|
||||
Детальные DTO и response shapes для экранов `Operations` и `Wizard` зафиксированы отдельно в:
|
||||
|
||||
`gRPC descriptor` endpoints добавляются отдельным этапом вместе с `grpc-support`.
|
||||
- `docs/operations-workspace-contracts.md`
|
||||
|
||||
### Agents
|
||||
|
||||
Нужны:
|
||||
|
||||
- CRUD агентов;
|
||||
- bindings к operations;
|
||||
- publish agent;
|
||||
- выдача MCP endpoint metadata.
|
||||
|
||||
### API Keys
|
||||
|
||||
Нужны:
|
||||
|
||||
- list/create/revoke/delete platform API keys;
|
||||
- one-time reveal значения ключа при создании.
|
||||
|
||||
### Logs
|
||||
|
||||
Нужны:
|
||||
|
||||
- list logs с фильтрами;
|
||||
- log detail;
|
||||
- polling или live refresh strategy.
|
||||
|
||||
### Usage
|
||||
|
||||
Нужны:
|
||||
|
||||
- агрегаты по периодам;
|
||||
- breakdown по operation;
|
||||
- breakdown по agent;
|
||||
- CSV export.
|
||||
|
||||
## 7. Принцип совместимости
|
||||
|
||||
Если UI расходится с текущим backend, приоритет отдается целевой продуктовой модели, но конфликт должен быть явно разобран в `docs/as-is-to-be.md` до начала реализации.
|
||||
|
||||
+184
-460
@@ -2,78 +2,161 @@
|
||||
|
||||
## 1. Назначение проекта
|
||||
|
||||
Проект представляет собой платформу для динамической публикации внешних API в виде MCP tools. Пользователь конфигурирует операцию через административный UI вместо написания отдельного backend-обработчика. Платформа сохраняет конфигурацию, валидирует ее, позволяет выполнить тестовый вызов и публикует операцию для использования LLM через MCP.
|
||||
Crank - платформа для публикации внешних API в виде MCP tools без написания отдельного backend-кода под каждую интеграцию. Пользователь конфигурирует интеграции через UI, а система:
|
||||
|
||||
Главная инженерная цель проекта - представить разные протоколы как единый набор операций с точки зрения MCP-слоя.
|
||||
- хранит и версионирует операции;
|
||||
- группирует их по workspace;
|
||||
- публикует их в составе конкретных agents;
|
||||
- выдает LLM не глобальный каталог tools, а curated toolset на один agent;
|
||||
- собирает продуктовые логи и usage по workspace, agent и operation.
|
||||
|
||||
## 2. Ключевой принцип проектирования
|
||||
## 2. Переход `As Is -> To Be`
|
||||
|
||||
Центральная абстракция системы - `Operation`.
|
||||
### 2.1. As Is
|
||||
|
||||
Каждая операция описывает один вызываемый элемент независимо от протокола:
|
||||
Текущее ядро системы построено вокруг:
|
||||
|
||||
- `name` - внутреннее уникальное имя.
|
||||
- `display_name` - имя, отображаемое в UI.
|
||||
- `protocol` - `rest`, `graphql` или `grpc`.
|
||||
- `target` - хост и протокол-специфичное описание назначения.
|
||||
- `input_schema` - нормализованный входной контракт.
|
||||
- `input_mapping` - правила отображения MCP-входа в поля целевого запроса.
|
||||
- `execution_config` - auth-профиль, таймауты, заголовки и протокол-специфичные параметры.
|
||||
- `output_mapping` - правила отображения ответа внешней системы в нормализованный выход.
|
||||
- `tool_description` - метаданные для MCP и LLM.
|
||||
- `status` - draft, testing, published, archived.
|
||||
- глобальной сущности `Operation`;
|
||||
- registry версий операций;
|
||||
- runtime adapters `REST / GraphQL / unary gRPC`;
|
||||
- `admin-api` для CRUD и тестовых вызовов;
|
||||
- `mcp-server`, который публикует tools из published operations.
|
||||
|
||||
MCP server должен понимать только нормализованный контракт. Протокольные адаптеры должны преобразовывать нормализованную модель в конкретный REST, GraphQL или gRPC вызов и затем возвращать ответ обратно в нормализованный JSON.
|
||||
### 2.2. To Be
|
||||
|
||||
## 3. Границы продукта
|
||||
Целевая архитектура расширяет текущее ядро до модели:
|
||||
|
||||
### Входит в MVP
|
||||
- `Workspace` - tenant boundary;
|
||||
- `Operation` - интеграционный контракт;
|
||||
- `Agent` - curated MCP surface;
|
||||
- `Platform API key` и `Membership` - доступ к самой платформе;
|
||||
- `Invocation log` и `Usage rollup` - observability слой.
|
||||
|
||||
- Административный UI для создания и редактирования операций.
|
||||
- Динамический реестр операций.
|
||||
- Runtime-выполнение REST операций.
|
||||
- Runtime-выполнение GraphQL операций.
|
||||
- Runtime-выполнение unary gRPC методов.
|
||||
- Загрузка примеров `JSON` для ускоренного создания схем и mappings.
|
||||
- Импорт и экспорт конфигураций в `YAML`.
|
||||
- Тестирование операций до публикации.
|
||||
- Публикация MCP tools на основе данных из реестра.
|
||||
- Hot reload опубликованных операций без изменения backend-кода.
|
||||
`Operation` остается фундаментом интеграции, но tools больше не публикуются глобально. MCP публикует tools в контексте конкретного `workspace` и конкретного `agent`.
|
||||
|
||||
### Не входит в MVP
|
||||
## 3. Ключевые сущности и их роль
|
||||
|
||||
### `Workspace`
|
||||
|
||||
Изолирует:
|
||||
|
||||
- операции;
|
||||
- auth profiles;
|
||||
- agents;
|
||||
- platform API keys;
|
||||
- logs и usage;
|
||||
- пользователей и роли.
|
||||
|
||||
### `Operation`
|
||||
|
||||
Описывает один вызываемый элемент независимо от протокола:
|
||||
|
||||
- `name`
|
||||
- `display_name`
|
||||
- `protocol`
|
||||
- `target`
|
||||
- `input_schema`
|
||||
- `input_mapping`
|
||||
- `execution_config`
|
||||
- `output_mapping`
|
||||
- `tool_description`
|
||||
- `status`
|
||||
|
||||
### `Agent`
|
||||
|
||||
Является пользовательской MCP-поверхностью для LLM.
|
||||
|
||||
`Agent`:
|
||||
|
||||
- принадлежит одному workspace;
|
||||
- имеет `slug`, `display_name`, `description`, `status`;
|
||||
- ссылается на ограниченный набор published operations;
|
||||
- формирует отдельный MCP endpoint;
|
||||
- решает проблему "одному агенту нельзя отдавать 100 tools сразу".
|
||||
|
||||
### `Platform access`
|
||||
|
||||
Отдельный слой, не связанный с upstream auth:
|
||||
|
||||
- `User`
|
||||
- `Membership`
|
||||
- `Invitation`
|
||||
- `PlatformApiKey`
|
||||
|
||||
### `Observability`
|
||||
|
||||
Отдельный продуктовый слой:
|
||||
|
||||
- `InvocationLog`
|
||||
- `InvocationEvent`
|
||||
- `UsageRollup`
|
||||
- `LatencyStats`
|
||||
|
||||
## 4. Главный принцип проектирования
|
||||
|
||||
Система строится в три слоя:
|
||||
|
||||
1. `Operation` как низкоуровневый интеграционный контракт.
|
||||
2. `Agent` как curated набор published operations.
|
||||
3. `Workspace` как граница данных, доступа и observability.
|
||||
|
||||
Это позволяет:
|
||||
|
||||
- переиспользовать одну operation в нескольких agents;
|
||||
- ограничивать tool catalog для конкретного LLM-сценария;
|
||||
- изолировать данные команд;
|
||||
- строить logs и usage не глобально, а по tenant boundary.
|
||||
|
||||
## 5. Границы целевого MVP
|
||||
|
||||
### Входит
|
||||
|
||||
- `Workspace` как tenant boundary.
|
||||
- Операции `REST`, `GraphQL`, `unary gRPC`.
|
||||
- `Agent` и привязка операций к агенту.
|
||||
- Agent-scoped MCP endpoints.
|
||||
- Platform API keys.
|
||||
- Workspace-scoped auth profiles для upstream access.
|
||||
- Product logs и usage aggregates.
|
||||
- Импорт и экспорт operation-конфигураций в `YAML`.
|
||||
- Hot reload опубликованных agents и operations.
|
||||
|
||||
### Не входит
|
||||
|
||||
- gRPC streaming.
|
||||
- Полноценный импорт OpenAPI с автоматической генерацией маппинга.
|
||||
- Полноценный визуальный конструктор GraphQL-запросов.
|
||||
- SOAP.
|
||||
- Выполнение произвольного кода внутри mapping-правил.
|
||||
- Оркестрация нескольких операций в виде workflow.
|
||||
- Мультитенантность и биллинг.
|
||||
- Оркестрация workflow.
|
||||
- Биллинг.
|
||||
- Full RBAC policy engine.
|
||||
- Traffic splitting и deployment orchestration.
|
||||
|
||||
## 4. Пользовательский сценарий
|
||||
## 6. Пользовательские сценарии
|
||||
|
||||
Сценарий работы оператора должен быть одинаковым для всех протоколов:
|
||||
### Оператор операций
|
||||
|
||||
1. Выбрать протокол.
|
||||
2. Указать целевой хост или сервер.
|
||||
3. Выбрать или описать внешнюю операцию.
|
||||
4. Определить MCP-входные параметры.
|
||||
5. Сопоставить MCP-вход с внешним запросом.
|
||||
6. Сопоставить внешний ответ с MCP-выходом.
|
||||
7. Добавить описание для MCP и LLM.
|
||||
8. Выполнить тестовый вызов.
|
||||
9. Опубликовать операцию.
|
||||
1. Выбирает workspace.
|
||||
2. Создает или редактирует operation.
|
||||
3. Выполняет test run.
|
||||
4. Публикует operation version.
|
||||
5. Привязывает operation к одному или нескольким agents.
|
||||
|
||||
UI должен максимально скрывать протокольную сложность. REST endpoint, GraphQL operation и gRPC method должны отображаться для оператора как "операция с входными и выходными параметрами".
|
||||
### Оператор агентов
|
||||
|
||||
## 5. Стратегия по протоколам
|
||||
1. Создает agent.
|
||||
2. Выбирает набор published operations.
|
||||
3. Публикует agent.
|
||||
4. Получает MCP endpoint вида `/mcp/v1/{workspace}/{agent}`.
|
||||
|
||||
### Администратор workspace
|
||||
|
||||
1. Управляет API keys платформы.
|
||||
2. Управляет пользователями и ролями.
|
||||
3. Смотрит logs и usage.
|
||||
|
||||
## 7. Стратегия по протоколам
|
||||
|
||||
### REST
|
||||
|
||||
REST-адаптер является базовым и должен реализовываться первым.
|
||||
|
||||
Поддержка в MVP:
|
||||
|
||||
- `GET`
|
||||
- `POST`
|
||||
- `PUT`
|
||||
@@ -84,22 +167,9 @@ REST-адаптер является базовым и должен реализ
|
||||
- headers
|
||||
- JSON request body
|
||||
- JSON response body
|
||||
- аутентификация `Bearer`, `Basic` и API key
|
||||
|
||||
Пользователь настраивает:
|
||||
|
||||
- base URL,
|
||||
- HTTP method,
|
||||
- path template,
|
||||
- request mapping,
|
||||
- response mapping.
|
||||
|
||||
### GraphQL
|
||||
|
||||
Поддержка GraphQL в MVP должна быть намеренно упрощена.
|
||||
|
||||
Поддержка в MVP:
|
||||
|
||||
- `query`
|
||||
- `mutation`
|
||||
- endpoint URL
|
||||
@@ -108,56 +178,16 @@ REST-адаптер является базовым и должен реализ
|
||||
- variables mapping
|
||||
- извлечение результата из `data`
|
||||
|
||||
Пользователь настраивает:
|
||||
|
||||
- GraphQL endpoint,
|
||||
- шаблон операции,
|
||||
- схему переменных,
|
||||
- маппинг переменных,
|
||||
- путь к нужным данным в ответе.
|
||||
|
||||
Introspection может быть добавлен позже как вспомогательная функция UI, но первая рабочая версия системы не должна от него зависеть.
|
||||
|
||||
Ключевое ограничение GraphQL в проекте: одна MCP operation должна соответствовать одному конкретному GraphQL-запросу или mutation с заранее определенным selection set. Платформа не должна пытаться передавать LLM всю гибкость GraphQL, потому что LLM не должен формировать произвольный набор полей и произвольную структуру параметров для одного и того же tool.
|
||||
|
||||
С точки зрения MCP GraphQL в этой системе намеренно превращается в более жесткий интерфейс:
|
||||
|
||||
- один tool;
|
||||
- один шаблон `query` или `mutation`;
|
||||
- фиксированный набор входных параметров;
|
||||
- один предсказуемый формат ответа.
|
||||
|
||||
Фактически на слое MCP "универсальность" GraphQL сознательно сужается до модели, близкой к RPC или REST operation. Это делается для того, чтобы tool оставался понятным для LLM, валидируемым, предсказуемым по структуре ответа и пригодным для явного mapping.
|
||||
GraphQL в MCP публикуется как фиксированная операция с предсказуемой структурой ответа.
|
||||
|
||||
### gRPC
|
||||
|
||||
gRPC - наиболее сложный протокол в этом проекте, поэтому его нужно ограничить на раннем этапе.
|
||||
- только unary RPC;
|
||||
- `.proto` и `descriptor set`;
|
||||
- JSON-oriented schema model поверх protobuf;
|
||||
- без streaming.
|
||||
|
||||
Поддержка в MVP:
|
||||
|
||||
- только unary RPC,
|
||||
- загрузка `.proto`,
|
||||
- загрузка descriptor set,
|
||||
- опционально server reflection на более позднем этапе,
|
||||
- преобразование между нормализованным JSON и protobuf-сообщениями.
|
||||
|
||||
Рекомендуемый путь реализации:
|
||||
|
||||
1. Принимать descriptor set как основной машинно-читаемый источник схемы.
|
||||
2. Опционально принимать `.proto` для удобства оператора.
|
||||
3. Парсить descriptor во внутреннюю модель схемы, удобную для UI.
|
||||
4. Показывать services, methods, входные поля и выходные поля в виде структурированной формы.
|
||||
5. Позволять пользователю настраивать input и output mapping.
|
||||
|
||||
Такой подход превращает gRPC для оператора в тот же опыт, что и REST: выбрать метод, посмотреть параметры, сопоставить поля, протестировать, опубликовать.
|
||||
|
||||
Streaming gRPC сознательно не входит в рамки проекта. Платформа ориентирована на MCP tool invocation, а MCP tool в этой системе моделируется как сценарий `запрос -> один ответ`. LLM не работает с долгоживущими транспортными сессиями и не нуждается в обработке потока сообщений для такого типа интеграции. Поэтому `server streaming`, `client streaming` и `bidirectional streaming` исключаются как архитектурно избыточные для выбранной модели взаимодействия.
|
||||
|
||||
Тот же принцип применяется и к GraphQL: даже если внешний GraphQL endpoint допускает очень гибкий способ получения данных, в MCP публикуются только заранее зафиксированные операции с контролируемым входом и контролируемым ответом.
|
||||
|
||||
## 6. Работа с файлами и автогенерация черновика
|
||||
|
||||
Для упрощения конфигурирования система должна поддерживать загрузку файлов и примеров данных, из которых можно собрать стартовую конфигурацию operation.
|
||||
## 8. Работа с файлами и автогенерация черновика
|
||||
|
||||
Поддерживаемые источники:
|
||||
|
||||
@@ -168,368 +198,62 @@ Streaming gRPC сознательно не входит в рамки проек
|
||||
|
||||
Ожидаемый сценарий:
|
||||
|
||||
1. Оператор загружает пример входных данных и пример ответа.
|
||||
2. Система строит черновую схему входа и выхода.
|
||||
3. Система предлагает стартовый mapping по совпадающим или близким по структуре полям.
|
||||
4. Оператор вручную корректирует результат.
|
||||
5. Для точечной настройки используется `JSONPath`.
|
||||
6. Готовую конфигурацию можно экспортировать в `YAML` или импортировать обратно.
|
||||
1. оператор загружает артефакты;
|
||||
2. система строит черновую схему и mapping;
|
||||
3. оператор вручную корректирует результат;
|
||||
4. готовую конфигурацию можно экспортировать в `YAML`.
|
||||
|
||||
Для gRPC источником структуры является не пример JSON-сообщения, а `.proto` или descriptor set. Однако после преобразования protobuf-схемы во внутреннюю JSON-ориентированную модель пользовательский опыт должен оставаться тем же: видим структуру полей, получаем стартовый mapping, затем уточняем его вручную.
|
||||
## 9. Внутренняя модель данных
|
||||
|
||||
`YAML` используется как человекочитаемое представление конфигурации operation для:
|
||||
Базовые сущности:
|
||||
|
||||
- переноса между окружениями;
|
||||
- резервного копирования;
|
||||
- хранения в git;
|
||||
- редактирования вне UI;
|
||||
- пакетного импорта нескольких operation.
|
||||
- `Workspace`
|
||||
- `Operation`
|
||||
- `OperationVersion`
|
||||
- `Agent`
|
||||
- `AgentVersion`
|
||||
- `AgentOperationBinding`
|
||||
- `AuthProfile`
|
||||
- `PlatformApiKey`
|
||||
- `InvocationLog`
|
||||
- `UsageRollup`
|
||||
|
||||
Storage backend для sample-файлов, `.proto`, `descriptor set` и YAML import payload в MVP должен быть локальным файловым хранилищем приложения с явным `storage_ref`. В дальнейшем этот слой можно заменить на S3-compatible storage без изменения доменной модели.
|
||||
## 10. MCP publishing model
|
||||
|
||||
## 7. Внутренняя модель данных
|
||||
Публикация tools строится так:
|
||||
|
||||
Система должна приводить все данные к JSON-ориентированным структурам, чтобы UI, registry и MCP runtime работали с единым контрактом.
|
||||
1. `Operation` проходит versioning и publish.
|
||||
2. `Agent` собирает curated набор published operations.
|
||||
3. `MCP server` читает published view конкретного agent.
|
||||
4. `tools/list` и `tools/call` работают в контексте `workspace + agent`.
|
||||
|
||||
### Operation
|
||||
## 11. Observability
|
||||
|
||||
- `id`
|
||||
- `name`
|
||||
- `display_name`
|
||||
- `protocol`
|
||||
На каждый вызов tool сохраняются:
|
||||
|
||||
- `workspace_id`
|
||||
- `agent_id`
|
||||
- `operation_id`
|
||||
- `request_id`
|
||||
- `timestamp`
|
||||
- `status`
|
||||
- `target`
|
||||
- `input_schema`
|
||||
- `output_schema`
|
||||
- `input_mapping`
|
||||
- `output_mapping`
|
||||
- `execution_config`
|
||||
- `tool_description`
|
||||
- `created_at`
|
||||
- `updated_at`
|
||||
- `duration_ms`
|
||||
- `error_kind`
|
||||
- `request_preview`
|
||||
- `response_preview`
|
||||
|
||||
### Target
|
||||
Сверху строятся:
|
||||
|
||||
REST target:
|
||||
- logs page;
|
||||
- usage page;
|
||||
- периодические rollups;
|
||||
- latency and error aggregates.
|
||||
|
||||
- `base_url`
|
||||
- `method`
|
||||
- `path_template`
|
||||
## 12. Модель маппинга
|
||||
|
||||
GraphQL target:
|
||||
Платформе нужен отдельный слой маппинга:
|
||||
|
||||
- `endpoint`
|
||||
- `operation_type`
|
||||
- `operation_name`
|
||||
- `query_template`
|
||||
|
||||
gRPC target:
|
||||
|
||||
- `server_addr`
|
||||
- `package`
|
||||
- `service`
|
||||
- `method`
|
||||
- `descriptor_ref`
|
||||
- `descriptor_set_b64`
|
||||
|
||||
### Schema
|
||||
|
||||
Нормализованный формат схемы должен поддерживать:
|
||||
|
||||
- скалярные поля,
|
||||
- вложенные объекты,
|
||||
- массивы,
|
||||
- enum,
|
||||
- nullable-поля,
|
||||
- `oneof` для схем, пришедших из protobuf.
|
||||
|
||||
Транспортный формат между внутренними компонентами должен оставаться JSON, даже если конкретный адаптер под капотом работает с protobuf.
|
||||
|
||||
## 8. Модель маппинга
|
||||
|
||||
Платформе нужен отдельный слой маппинга, потому что MCP-facing параметры не совпадают напрямую с payload внешнего API.
|
||||
|
||||
Начальная версия mapping-системы должна оставаться простой, но при этом достаточно выразительной для работы со вложенными структурами:
|
||||
|
||||
- сопоставление поле-в-поле по `JSONPath`,
|
||||
- константы,
|
||||
- значения по умолчанию,
|
||||
- сопоставление поле-в-поле по `JSONPath`;
|
||||
- константы;
|
||||
- значения по умолчанию;
|
||||
- извлечение вложенных полей из ответа.
|
||||
|
||||
Примеры:
|
||||
|
||||
- `$.mcp.user_id -> $.request.path.userId`
|
||||
- `$.mcp.limit -> $.request.query.limit`
|
||||
- `$.response.data.user.name -> $.output.name`
|
||||
- `$.response.user.email -> $.output.email`
|
||||
|
||||
`JSONPath` используется как единый способ адресации вложенных значений в input/output mapping. Это позволяет управлять структурой и вложенностью без написания пользовательского кода.
|
||||
|
||||
Для MVP mapping engine не должен поддерживать произвольные скрипты. Достаточно единого движка `JSONPath`, констант, defaults и ограниченного набора встроенных преобразований.
|
||||
|
||||
Черновой mapping может генерироваться автоматически на основе загруженных примеров данных, но итоговая конфигурация всегда остается явной и редактируемой оператором.
|
||||
|
||||
Для MVP допустима простая стратегия draft generation:
|
||||
|
||||
- искать уникальные leaf-поля с одинаковыми именами;
|
||||
- предлагать только однозначные соответствия;
|
||||
- не пытаться автоматически разрешать конфликты и неоднозначности.
|
||||
|
||||
Каноническая логическая модель остается общей для runtime и БД, но система должна уметь сериализовать и десериализовать ее также в `YAML`.
|
||||
|
||||
## 9. Основные компоненты
|
||||
|
||||
### `crank-core`
|
||||
|
||||
Ответственность:
|
||||
|
||||
- общие доменные типы,
|
||||
- идентификаторы,
|
||||
- статусы и базовые protocol-specific target types,
|
||||
- общие ошибки.
|
||||
|
||||
### `crank-schema`
|
||||
|
||||
Ответственность:
|
||||
|
||||
- нормализованные схемы входа и выхода,
|
||||
- представление типов и полей для UI и runtime,
|
||||
- валидация JSON относительно внутренней схемы.
|
||||
|
||||
### `crank-mapping`
|
||||
|
||||
Ответственность:
|
||||
|
||||
- модель mapping-правил,
|
||||
- `JSONPath` parser и validator,
|
||||
- применение input/output mapping,
|
||||
- генерация чернового mapping по sample-данным и схемам.
|
||||
|
||||
### `crank-proto`
|
||||
|
||||
Ответственность:
|
||||
|
||||
- загрузка `.proto` и `descriptor set`,
|
||||
- protobuf discovery,
|
||||
- извлечение services, methods и message schemas,
|
||||
- преобразование protobuf metadata в нормализованные схемы.
|
||||
|
||||
### `crank-registry`
|
||||
|
||||
Ответственность:
|
||||
|
||||
- постоянное хранение операций,
|
||||
- CRUD для draft и published операций,
|
||||
- выдача списка активных tools,
|
||||
- инвалидация кэша и сигналы на reload.
|
||||
|
||||
### `crank-runtime`
|
||||
|
||||
Ответственность:
|
||||
|
||||
- выполнение нормализованных операций,
|
||||
- выбор нужного протокольного адаптера,
|
||||
- применение input mapping,
|
||||
- применение output mapping,
|
||||
- единообразные runtime-ошибки.
|
||||
|
||||
### `crank-adapter-rest`
|
||||
|
||||
Ответственность:
|
||||
|
||||
- сборка HTTP-запроса из нормализованного входа,
|
||||
- отправка запроса через `reqwest`,
|
||||
- нормализация HTTP-ответа в JSON.
|
||||
|
||||
### `crank-adapter-graphql`
|
||||
|
||||
Ответственность:
|
||||
|
||||
- формирование GraphQL payload,
|
||||
- подстановка переменных,
|
||||
- отправка запроса,
|
||||
- извлечение `data` и ошибок из GraphQL-ответа.
|
||||
|
||||
### `crank-adapter-grpc`
|
||||
|
||||
Ответственность:
|
||||
|
||||
- сборка protobuf request message из нормализованного JSON,
|
||||
- вызов unary RPC метода,
|
||||
- преобразование protobuf response обратно в нормализованный JSON.
|
||||
|
||||
### `crank-admin-api`
|
||||
|
||||
Ответственность:
|
||||
|
||||
- CRUD endpoints для UI,
|
||||
- создание и управление version snapshots,
|
||||
- import/export конфигураций в `YAML`,
|
||||
- загрузка sample JSON,
|
||||
- загрузка `.proto` и descriptor set,
|
||||
- endpoints для тестового выполнения операций,
|
||||
- discovery endpoints для gRPC metadata.
|
||||
|
||||
### `crank-mcp-server`
|
||||
|
||||
Ответственность:
|
||||
|
||||
- список доступных MCP tools из registry,
|
||||
- валидация входа tool по нормализованной схеме,
|
||||
- делегирование выполнения в runtime,
|
||||
- возврат нормализованного результата MCP-клиенту.
|
||||
|
||||
### `crank-ui`
|
||||
|
||||
Ответственность:
|
||||
|
||||
- wizard создания сервиса и операции,
|
||||
- editor для mapping,
|
||||
- загрузка sample-файлов и schema artifacts,
|
||||
- экран тестового вызова,
|
||||
- браузер gRPC схемы,
|
||||
- import/export конфигураций,
|
||||
- workflow публикации и отображение статуса.
|
||||
|
||||
### Deployment layer
|
||||
|
||||
Ответственность:
|
||||
|
||||
- контейнерная упаковка приложений;
|
||||
- orchestration через `docker-compose`;
|
||||
- reverse proxy routing;
|
||||
- healthchecks и delivery pipeline.
|
||||
|
||||
Этот слой не должен влиять на доменную модель и application contracts.
|
||||
|
||||
## 10. Предлагаемая структура репозитория
|
||||
|
||||
Для реализации рекомендуется workspace-структура:
|
||||
|
||||
```text
|
||||
crank/
|
||||
apps/
|
||||
admin-api/
|
||||
mcp-server/
|
||||
ui/
|
||||
crates/
|
||||
crank-core/
|
||||
crank-schema/
|
||||
crank-mapping/
|
||||
crank-proto/
|
||||
crank-registry/
|
||||
crank-runtime/
|
||||
crank-adapter-rest/
|
||||
crank-adapter-graphql/
|
||||
crank-adapter-grpc/
|
||||
docs/
|
||||
```
|
||||
|
||||
Такая структура позволяет держать протокольные адаптеры независимыми и отдельно тестируемыми.
|
||||
|
||||
## 11. Технологический стек
|
||||
|
||||
### Backend
|
||||
|
||||
- Rust
|
||||
- `tokio` как async runtime
|
||||
- `axum` для HTTP API
|
||||
- `serde` и `serde_json`
|
||||
- `sqlx` для PostgreSQL
|
||||
- `reqwest` для REST и GraphQL транспорта
|
||||
- `tonic` и `prost` для работы с gRPC
|
||||
- `tower` для middleware
|
||||
- `tracing` для логирования и диагностики
|
||||
|
||||
### Frontend
|
||||
|
||||
- TypeScript
|
||||
- React
|
||||
- Vite
|
||||
- React Router
|
||||
- TanStack Query
|
||||
- React Hook Form
|
||||
- Zod
|
||||
|
||||
Этот стек прагматичен для внутреннего административного UI: быстрая итерация, удобная работа с формами, понятная интеграция с API и отсутствие лишней сложности.
|
||||
|
||||
## 12. Почему React + Vite для UI
|
||||
|
||||
Frontend в этом проекте - это операторская консоль, а не контентный сайт. Server-side rendering здесь не требуется. Основные требования:
|
||||
|
||||
- динамические формы,
|
||||
- schema-driven рендеринг,
|
||||
- экраны тестирования и предпросмотра,
|
||||
- адаптивные административные страницы,
|
||||
- высокая скорость локальной разработки.
|
||||
|
||||
`React + TypeScript + Vite` хорошо подходит под эти условия, потому что позволяет развивать frontend независимо от Rust-сервисов и быстро собирать сложные формы вроде mapping editor и gRPC method inspector.
|
||||
|
||||
## 13. Почему Axum для backend
|
||||
|
||||
`axum` выбран как основной backend-фреймворк по следующим причинам:
|
||||
|
||||
- он построен поверх `tower` и хорошо согласуется с современным async-стеком Rust,
|
||||
- он естественно интегрируется с `tokio`, `hyper` и middleware-композицией,
|
||||
- он лучше подходит для модульной структуры с несколькими сервисами,
|
||||
- он удобен для typed handlers, shared state и собственных extractors,
|
||||
- он лучше сочетается с `tonic`, который используется для gRPC.
|
||||
|
||||
Детальная декомпозиция crates и модулей вынесена в `docs/module-decomposition.md`.
|
||||
Формальная модель данных вынесена в `docs/data-model.md`.
|
||||
Схема БД и versioning описаны в `docs/database-schema.md`.
|
||||
HTTP-контракты административного API описаны в `docs/admin-api.md`.
|
||||
Диаграммы компонентов, сущностей и потоков вынесены в `docs/diagrams.md`.
|
||||
MCP transport и способ публикации tools описаны в `docs/mcp-interface.md`.
|
||||
Стратегия тестирования описана в `docs/testing-strategy.md`, а runtime-конфигурация и storage assumptions - в `docs/runtime-config.md`.
|
||||
Rust-oriented распределение методов, `impl`, `trait` и service-слоя описано в `docs/rust-design.md`.
|
||||
Правила разработки и TDD-процесс описаны в `docs/development-rules.md`, а последовательность модулей и фич - в `docs/implementation-plan.md`.
|
||||
Rust-specific правила кода, linting и toolchain описаны в `docs/rust-code-rules.md`.
|
||||
Требования и ограничения по конкретным протоколам вынесены в `docs/protocols/rest.md`, `docs/protocols/graphql.md` и `docs/protocols/grpc.md`.
|
||||
|
||||
## 14. Runtime-поток
|
||||
|
||||
### Создание операции
|
||||
|
||||
1. UI отправляет draft операции в admin API.
|
||||
2. Admin API валидирует схему и mappings.
|
||||
3. Registry сохраняет draft.
|
||||
4. UI запускает тестовый вызов через runtime.
|
||||
5. Оператор публикует операцию.
|
||||
6. Registry помечает операцию как active.
|
||||
7. MCP server перезагружает активные операции.
|
||||
|
||||
### Выполнение tool
|
||||
|
||||
1. MCP client вызывает tool.
|
||||
2. MCP server берет определение tool из памяти.
|
||||
3. Runtime валидирует вход относительно нормализованной схемы.
|
||||
4. Runtime применяет input mapping.
|
||||
5. Runtime вызывает нужный протокольный адаптер.
|
||||
6. Runtime применяет output mapping.
|
||||
7. MCP server возвращает нормализованный результат.
|
||||
|
||||
Эта последовательность соответствует модели `один запрос -> один ответ`. Именно поэтому поддержка streaming-протоколов не рассматривается как часть MVP: она не соответствует целевой модели вызова tools со стороны LLM.
|
||||
По этой же причине GraphQL tools должны быть заранее специализированы под конкретный сценарий вызова, а не представлять собой общий конструктор запросов для LLM.
|
||||
|
||||
## 15. Нефункциональные требования
|
||||
|
||||
- Новые операции должны добавляться без изменения backend-кода.
|
||||
- Опубликованные операции должны становиться видимыми для MCP-клиентов без пересборки сервиса.
|
||||
- Runtime-ошибки должны быть наблюдаемыми и различимыми по этапам.
|
||||
- Система должна оставаться детерминированной и пригодной для аудита.
|
||||
- Протокольные адаптеры должны тестироваться независимо.
|
||||
- Все опубликованные операции должны укладываться в модель синхронного или квазисинхронного вызова `запрос -> ответ`.
|
||||
- Все опубликованные GraphQL operations должны иметь фиксированный шаблон запроса и фиксированную структуру ожидаемого результата.
|
||||
|
||||
## 16. Основные риски
|
||||
|
||||
- Динамическая работа с protobuf заметно сложнее, чем REST и GraphQL.
|
||||
- UX для маппинга может стать слишком тяжелым, если не ограничить его заранее.
|
||||
- Нормализация схем может стать непоследовательной без строгой внутренней модели.
|
||||
- Попытка поддержать слишком много возможностей протоколов замедлит реализацию.
|
||||
- Попытка сохранить всю динамическую гибкость GraphQL на уровне MCP приведет к слишком широким и плохо управляемым tools.
|
||||
|
||||
Поэтому проект должен в первую очередь реализовать один чистый end-to-end сценарий, а не широкий, но поверхностный охват возможностей.
|
||||
|
||||
`SOAP` в этой версии проекта сознательно отложен. Это не забытый протокол, а отдельное направление развития, которое потребует самостоятельного XML/WSDL слоя, отдельной схемной модели и отдельного адаптера.
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
# As Is -> To Be
|
||||
|
||||
## 1. Назначение документа
|
||||
|
||||
Этот документ фиксирует переход от текущего состояния проекта к целевой продуктовой модели, которую задает `test-ui`.
|
||||
|
||||
## 2. As Is
|
||||
|
||||
Сейчас проект умеет:
|
||||
|
||||
- хранить и версионировать `Operation`;
|
||||
- выполнять `REST`, `GraphQL`, `unary gRPC`;
|
||||
- выполнять test run;
|
||||
- публиковать operations в MCP;
|
||||
- импортировать и экспортировать operation-конфигурации;
|
||||
- загружать samples и gRPC descriptors.
|
||||
|
||||
Сейчас проект не умеет как first-class product features:
|
||||
|
||||
- `Workspace`
|
||||
- `Agent`
|
||||
- platform API keys
|
||||
- members / invitations
|
||||
- logs API
|
||||
- usage API
|
||||
- agent-scoped MCP toolsets
|
||||
|
||||
## 3. To Be
|
||||
|
||||
Целевая система должна работать так:
|
||||
|
||||
- каждая команда работает в своем `Workspace`;
|
||||
- операции создаются и тестируются внутри workspace;
|
||||
- опубликованные операции привязываются к `Agent`;
|
||||
- один `Agent` отдает LLM ограниченный набор tools;
|
||||
- доступ к платформе контролируется через memberships и platform API keys;
|
||||
- все вызовы попадают в logs и usage.
|
||||
|
||||
## 4. Page-by-page gap analysis
|
||||
|
||||
### 4.1. Operations
|
||||
|
||||
Что хочет UI:
|
||||
|
||||
- список операций;
|
||||
- фильтры;
|
||||
- edit/delete;
|
||||
- publish/archive;
|
||||
- верхние метрики;
|
||||
- фильтр по agent.
|
||||
|
||||
Что уже есть:
|
||||
|
||||
- list/create/version/publish/test/export/import;
|
||||
- samples и draft generation.
|
||||
|
||||
Чего не хватает:
|
||||
|
||||
- delete operation;
|
||||
- archive operation;
|
||||
- usage summary для карточек;
|
||||
- связь operation с agent;
|
||||
- workspace scoping.
|
||||
|
||||
### 4.2. Wizard
|
||||
|
||||
Что хочет UI:
|
||||
|
||||
- create/edit operation;
|
||||
- сохранить draft;
|
||||
- тестировать и потом публиковать;
|
||||
- работать с `REST / GraphQL / gRPC`;
|
||||
- descriptors и schema-driven setup.
|
||||
|
||||
Что уже есть:
|
||||
|
||||
- почти весь operation lifecycle;
|
||||
- samples;
|
||||
- draft generation;
|
||||
- gRPC descriptor workflow.
|
||||
|
||||
Чего не хватает:
|
||||
|
||||
- нормальный update flow без local storage;
|
||||
- workspace-scoped endpoints;
|
||||
- единый backend contract под final wizard shape.
|
||||
|
||||
### 4.3. Agents
|
||||
|
||||
Что хочет UI:
|
||||
|
||||
- каталог агентов;
|
||||
- create/edit agent;
|
||||
- выбрать список operations;
|
||||
- получить MCP endpoint агента.
|
||||
|
||||
Что уже есть:
|
||||
|
||||
- ничего как отдельный product layer.
|
||||
|
||||
Чего не хватает:
|
||||
|
||||
- сущность `Agent`;
|
||||
- `AgentVersion`;
|
||||
- `AgentOperationBinding`;
|
||||
- publish agent;
|
||||
- agent-scoped MCP runtime.
|
||||
|
||||
### 4.4. API Keys
|
||||
|
||||
Что хочет UI:
|
||||
|
||||
- list/create/revoke/delete platform API keys;
|
||||
- scopes;
|
||||
- one-time reveal.
|
||||
|
||||
Что уже есть:
|
||||
|
||||
- только upstream `auth_profiles`.
|
||||
|
||||
Чего не хватает:
|
||||
|
||||
- отдельная сущность `PlatformApiKey`;
|
||||
- hashing/secrets;
|
||||
- scopes model;
|
||||
- endpoints и audit.
|
||||
|
||||
### 4.5. Logs
|
||||
|
||||
Что хочет UI:
|
||||
|
||||
- список логов;
|
||||
- detail view;
|
||||
- filters;
|
||||
- live mode.
|
||||
|
||||
Что уже есть:
|
||||
|
||||
- только application logging.
|
||||
|
||||
Чего не хватает:
|
||||
|
||||
- продуктовая сущность `InvocationLog`;
|
||||
- storage;
|
||||
- list/detail API;
|
||||
- polling/live refresh strategy.
|
||||
|
||||
### 4.6. Usage
|
||||
|
||||
Что хочет UI:
|
||||
|
||||
- usage dashboard;
|
||||
- p50/p95/p99;
|
||||
- error rate;
|
||||
- per-operation breakdown;
|
||||
- CSV export.
|
||||
|
||||
Что уже есть:
|
||||
|
||||
- продуктового usage слоя нет.
|
||||
|
||||
Чего не хватает:
|
||||
|
||||
- `UsageRollup`;
|
||||
- aggregation jobs;
|
||||
- reporting API;
|
||||
- export endpoint.
|
||||
|
||||
### 4.7. Workspace / Settings
|
||||
|
||||
Что хочет UI:
|
||||
|
||||
- create/edit workspace;
|
||||
- members and invitations;
|
||||
- settings.
|
||||
|
||||
Что уже есть:
|
||||
|
||||
- ничего как backend model.
|
||||
|
||||
Чего не хватает:
|
||||
|
||||
- `Workspace`;
|
||||
- `User`;
|
||||
- `Membership`;
|
||||
- `Invitation`;
|
||||
- workspace-scoped routing.
|
||||
|
||||
### 4.8. Login
|
||||
|
||||
Что хочет UI:
|
||||
|
||||
- platform sign-in flow.
|
||||
|
||||
Что уже есть:
|
||||
|
||||
- внешний `Basic Auth` на уровне `nginx`.
|
||||
|
||||
Чего не хватает:
|
||||
|
||||
- либо собственный auth/session backend;
|
||||
- либо временный согласованный bridge, если login screen оставляем как demo flow.
|
||||
|
||||
## 5. Архитектурные конфликты, которые нужно разобрать отдельно
|
||||
|
||||
### Конфликт 1. Global operations vs workspace model
|
||||
|
||||
Решение:
|
||||
|
||||
- ввести `workspace_id` во все продуктовые сущности.
|
||||
|
||||
### Конфликт 2. Published operations vs agents
|
||||
|
||||
Решение:
|
||||
|
||||
- MCP публикует tools не напрямую из operations, а из `published agent`.
|
||||
|
||||
### Конфликт 3. Upstream auth vs platform API keys
|
||||
|
||||
Решение:
|
||||
|
||||
- оставить `AuthProfile` только для upstream;
|
||||
- ввести отдельную сущность `PlatformApiKey`.
|
||||
|
||||
### Конфликт 4. Application logs vs product logs
|
||||
|
||||
Решение:
|
||||
|
||||
- ввести `InvocationLog` и `UsageRollup`.
|
||||
|
||||
### Конфликт 5. Basic Auth vs login page
|
||||
|
||||
Решение:
|
||||
|
||||
- зафиксировать временную и целевую auth model отдельно.
|
||||
|
||||
## 6. Приоритет реализации
|
||||
|
||||
### Wave 1
|
||||
|
||||
- Workspace foundation
|
||||
- Operations + Wizard integration
|
||||
- Agents
|
||||
- Agent-scoped MCP
|
||||
|
||||
### Wave 2
|
||||
|
||||
- Platform API keys
|
||||
- Logs
|
||||
- Usage
|
||||
|
||||
### Wave 3
|
||||
|
||||
- Members / invitations
|
||||
- Login / session layer
|
||||
|
||||
## 7. Короткий итог
|
||||
|
||||
Целевой UI не требует выбросить текущее ядро. Он требует добавить сверху:
|
||||
|
||||
- tenant layer;
|
||||
- curated agent layer;
|
||||
- observability layer;
|
||||
- platform access layer.
|
||||
@@ -0,0 +1,343 @@
|
||||
# Backend Gap Plan
|
||||
|
||||
## 1. Назначение документа
|
||||
|
||||
Этот документ превращает `test-ui` и `docs/as-is-to-be.md` в конкретный backend-план.
|
||||
|
||||
Его задача:
|
||||
|
||||
- зафиксировать, чего именно не хватает в backend;
|
||||
- разделить доработки на новые сущности, новые API и изменения существующего ядра;
|
||||
- определить порядок реализации без расползания scope.
|
||||
|
||||
## 2. Принципы
|
||||
|
||||
- текущее ядро `Operation + adapters + runtime` сохраняется;
|
||||
- новая функциональность наращивается слоями сверху;
|
||||
- сначала вводятся tenant boundary и agent publishing;
|
||||
- потом access layer и observability;
|
||||
- перенос `test-ui` в `apps/ui` делается после фиксации backend-контрактов.
|
||||
|
||||
## 3. Что сохраняем без переписывания
|
||||
|
||||
- `Operation` как базовый интеграционный контракт;
|
||||
- versioning операций;
|
||||
- adapters `REST / GraphQL / unary gRPC`;
|
||||
- schema engine;
|
||||
- mapping engine;
|
||||
- YAML import/export для operations;
|
||||
- test-run flow;
|
||||
- `Streamable HTTP` transport.
|
||||
|
||||
## 4. Что меняется принципиально
|
||||
|
||||
### 4.1. Global model -> workspace-scoped model
|
||||
|
||||
Нужно добавить `workspace_id` в:
|
||||
|
||||
- `operations`
|
||||
- `auth_profiles`
|
||||
- published runtime views
|
||||
- будущие logs и usage
|
||||
|
||||
### 4.2. Operation publishing -> agent publishing
|
||||
|
||||
Нужно перестроить MCP publishing:
|
||||
|
||||
- было: published operation = MCP tool
|
||||
- будет: published operation = reusable building block
|
||||
- MCP endpoint публикует published bindings внутри конкретного agent
|
||||
|
||||
### 4.3. Upstream auth -> platform access separation
|
||||
|
||||
Нужно разделить:
|
||||
|
||||
- `AuthProfile` для внешних API;
|
||||
- `PlatformApiKey` для доступа к Crank.
|
||||
|
||||
### 4.4. Application logs -> product observability
|
||||
|
||||
Нужно ввести:
|
||||
|
||||
- `InvocationLog`
|
||||
- `UsageRollup`
|
||||
|
||||
## 5. Page-by-page backend plan
|
||||
|
||||
## 5.1. Operations page
|
||||
|
||||
### Уже есть
|
||||
|
||||
- `GET /operations`
|
||||
- `POST /operations`
|
||||
- `GET /operations/{id}`
|
||||
- `POST /operations/{id}/versions`
|
||||
- `POST /operations/{id}/publish`
|
||||
- `POST /operations/{id}/test-runs`
|
||||
- import/export
|
||||
|
||||
### Не хватает
|
||||
|
||||
- `PATCH /operations/{id}`
|
||||
- `DELETE /operations/{id}`
|
||||
- `POST /operations/{id}/archive`
|
||||
- workspace scoping
|
||||
- summary metrics для карточек
|
||||
- фильтр/lookup по agent bindings
|
||||
|
||||
### Backend changes
|
||||
|
||||
- добавить update/delete/archive use case;
|
||||
- добавить `workspace_id` в operation queries;
|
||||
- добавить lightweight stats response для operations list.
|
||||
|
||||
## 5.2. Wizard page
|
||||
|
||||
### Уже есть
|
||||
|
||||
- create operation
|
||||
- create version
|
||||
- samples
|
||||
- draft generation
|
||||
- test run
|
||||
- gRPC descriptor flow
|
||||
|
||||
### Не хватает
|
||||
|
||||
- единый update contract для edit mode;
|
||||
- стабильный draft-save contract;
|
||||
- workspace-scoped endpoints;
|
||||
- final DTO shape под новый Alpine wizard.
|
||||
|
||||
### Backend changes
|
||||
|
||||
- нормализовать create/update/version payload;
|
||||
- добавить `PATCH /operations/{id}`;
|
||||
- оставить `POST /versions` как explicit publishable snapshot flow.
|
||||
|
||||
## 5.3. Agents page
|
||||
|
||||
### Уже есть
|
||||
|
||||
- ничего
|
||||
|
||||
### Не хватает
|
||||
|
||||
- `Agent`
|
||||
- `AgentVersion`
|
||||
- `AgentOperationBinding`
|
||||
- `published_agents`
|
||||
- MCP metadata per agent
|
||||
|
||||
### Backend changes
|
||||
|
||||
- CRUD agents;
|
||||
- publish agent;
|
||||
- bind/unbind operations;
|
||||
- list agent operations;
|
||||
- agent-scoped tool catalog in MCP.
|
||||
|
||||
## 5.4. API Keys page
|
||||
|
||||
### Уже есть
|
||||
|
||||
- upstream `auth_profiles`, но это другая сущность
|
||||
|
||||
### Не хватает
|
||||
|
||||
- `PlatformApiKey`
|
||||
- scopes model
|
||||
- one-time reveal on create
|
||||
- revoke/delete
|
||||
|
||||
### Backend changes
|
||||
|
||||
- новая таблица и новый service;
|
||||
- hash secret вместо хранения в открытом виде;
|
||||
- endpoint create/list/revoke/delete;
|
||||
- timestamp `last_used_at`.
|
||||
|
||||
## 5.5. Logs page
|
||||
|
||||
### Уже есть
|
||||
|
||||
- только application logs
|
||||
|
||||
### Не хватает
|
||||
|
||||
- список invocation logs
|
||||
- log detail
|
||||
- фильтры
|
||||
- live refresh strategy
|
||||
|
||||
### Backend changes
|
||||
|
||||
- логирование каждого tool call;
|
||||
- storage для invocation logs;
|
||||
- list/detail API;
|
||||
- query params: level, operation, agent, range, search.
|
||||
|
||||
## 5.6. Usage page
|
||||
|
||||
### Уже есть
|
||||
|
||||
- ничего как продуктовый API
|
||||
|
||||
### Не хватает
|
||||
|
||||
- aggregates
|
||||
- per-agent breakdown
|
||||
- per-operation breakdown
|
||||
- CSV export
|
||||
|
||||
### Backend changes
|
||||
|
||||
- `UsageRollup`;
|
||||
- background aggregation или on-write update strategy;
|
||||
- reporting endpoints;
|
||||
- CSV export endpoint.
|
||||
|
||||
## 5.7. Workspace and Settings
|
||||
|
||||
### Уже есть
|
||||
|
||||
- ничего
|
||||
|
||||
### Не хватает
|
||||
|
||||
- `Workspace`
|
||||
- `User`
|
||||
- `Membership`
|
||||
- `Invitation`
|
||||
- settings model
|
||||
|
||||
### Backend changes
|
||||
|
||||
- workspace CRUD;
|
||||
- members list;
|
||||
- invitations;
|
||||
- current workspace settings read/update.
|
||||
|
||||
## 5.8. Login
|
||||
|
||||
### Уже есть
|
||||
|
||||
- только внешний `Basic Auth` на `nginx`
|
||||
|
||||
### Не хватает
|
||||
|
||||
- app-level auth model
|
||||
|
||||
### Решение на ближайший этап
|
||||
|
||||
- пока не строить full auth subsystem;
|
||||
- сначала сделать workspace/access model и platform API keys;
|
||||
- login page оставить как отдельный вопрос после backend foundation.
|
||||
|
||||
## 6. Новые доменные сущности
|
||||
|
||||
Нужно добавить:
|
||||
|
||||
- `Workspace`
|
||||
- `User`
|
||||
- `Membership`
|
||||
- `Invitation`
|
||||
- `Agent`
|
||||
- `AgentVersion`
|
||||
- `AgentOperationBinding`
|
||||
- `PlatformApiKey`
|
||||
- `InvocationLog`
|
||||
- `UsageRollup`
|
||||
|
||||
## 7. Новые таблицы
|
||||
|
||||
Нужно добавить:
|
||||
|
||||
- `workspaces`
|
||||
- `users`
|
||||
- `memberships`
|
||||
- `invitation_tokens`
|
||||
- `agents`
|
||||
- `agent_versions`
|
||||
- `agent_operation_bindings`
|
||||
- `published_agents`
|
||||
- `platform_api_keys`
|
||||
- `invocation_logs`
|
||||
- `usage_rollups`
|
||||
|
||||
Нужно изменить:
|
||||
|
||||
- `operations`
|
||||
- `auth_profiles`
|
||||
- `published_operations`
|
||||
|
||||
## 8. Новые admin-api группы
|
||||
|
||||
Нужно добавить группы:
|
||||
|
||||
- `workspaces`
|
||||
- `memberships`
|
||||
- `invitations`
|
||||
- `agents`
|
||||
- `platform-api-keys`
|
||||
- `logs`
|
||||
- `usage`
|
||||
|
||||
Нужно расширить:
|
||||
|
||||
- `operations`
|
||||
- `auth-profiles`
|
||||
|
||||
## 9. MCP-server изменения
|
||||
|
||||
Нужно изменить:
|
||||
|
||||
- routing model: `/mcp/v1/{workspace_slug}/{agent_slug}`
|
||||
- runtime catalog source: `published_agents`, а не глобальные operations
|
||||
- tool resolution через binding
|
||||
- labels для logs и usage: `workspace`, `agent`, `operation`
|
||||
|
||||
## 10. Порядок реализации
|
||||
|
||||
### Wave 1. Foundation
|
||||
|
||||
- `Workspace`
|
||||
- workspace scope в operations/auth profiles
|
||||
- `PATCH/DELETE/ARCHIVE` для operations
|
||||
- update contracts для wizard
|
||||
|
||||
### Wave 2. Agent publishing
|
||||
|
||||
- `Agent`
|
||||
- `AgentVersion`
|
||||
- bindings
|
||||
- published agents
|
||||
- MCP server v2
|
||||
|
||||
### Wave 3. Platform access
|
||||
|
||||
- `PlatformApiKey`
|
||||
- memberships
|
||||
- invitations
|
||||
- workspace settings
|
||||
|
||||
### Wave 4. Observability
|
||||
|
||||
- invocation logs
|
||||
- usage rollups
|
||||
- logs API
|
||||
- usage API
|
||||
|
||||
### Wave 5. UI integration
|
||||
|
||||
- заменить mock data реальными API
|
||||
- перенести `test-ui` в `apps/ui`
|
||||
- пройти e2e сценарии
|
||||
|
||||
## 11. Что делаем следующим шагом
|
||||
|
||||
Следующий практический шаг:
|
||||
|
||||
1. зафиксировать DTO и response shapes для `Operations` и `Wizard`;
|
||||
2. затем спроектировать `Workspace` и `Agent` storage/API;
|
||||
3. после этого начинать backend implementation wave 1.
|
||||
+189
-655
@@ -2,159 +2,210 @@
|
||||
|
||||
## 1. Назначение документа
|
||||
|
||||
Этот документ фиксирует формальную модель данных платформы. Его цель - определить такие структуры, которые можно без существенных изменений перенести в:
|
||||
Этот документ фиксирует целевую формальную модель данных платформы. Его цель - определить такие структуры, которые можно без существенных изменений перенести в:
|
||||
|
||||
- Rust domain types,
|
||||
- HTTP DTO,
|
||||
- структуру таблиц БД,
|
||||
- runtime-представление operation,
|
||||
- Rust domain types;
|
||||
- HTTP DTO;
|
||||
- структуру таблиц БД;
|
||||
- runtime-представление операций и агентов;
|
||||
- UI-формы и конфигурационные экраны.
|
||||
|
||||
Документ не привязан к конкретной СУБД, но задает каноническую JSON-модель сущностей.
|
||||
|
||||
## 2. Общие принципы модели
|
||||
|
||||
### 2.1. Одна операция - один tool
|
||||
### 2.1. Одна операция - один интеграционный контракт
|
||||
|
||||
Каждая `Operation` соответствует одному MCP tool. Это особенно важно для:
|
||||
Каждая `Operation` соответствует одному интеграционному контракту:
|
||||
|
||||
- GraphQL, где одна operation соответствует одному конкретному `query` или `mutation`;
|
||||
- gRPC, где одна operation соответствует одному unary-методу;
|
||||
- REST, где одна operation соответствует одному endpoint-сценарию.
|
||||
- GraphQL -> один конкретный `query` или `mutation`;
|
||||
- gRPC -> один unary method;
|
||||
- REST -> один endpoint-сценарий.
|
||||
|
||||
Однако MCP tool публикуется не напрямую из operation, а через `AgentOperationBinding` внутри конкретного `Agent`.
|
||||
|
||||
### 2.2. Внутренний транспортный формат - JSON
|
||||
|
||||
Независимо от внешнего протокола внутри системы данные должны быть представлены в JSON-ориентированном виде. Даже если внешний вызов работает с protobuf, runtime, mapping и UI опираются на нормализованный JSON.
|
||||
Независимо от внешнего протокола внутри системы данные представлены в JSON-ориентированном виде.
|
||||
|
||||
### 2.3. Mapping всегда явный
|
||||
|
||||
Даже если система умеет строить черновой mapping по примерам данных, итоговая конфигурация mapping должна быть явно сохранена в operation. Нельзя полагаться на неявную "магию" сопоставления во время выполнения.
|
||||
Даже если система умеет строить черновой mapping по примерам данных, итоговая конфигурация mapping сохраняется явно.
|
||||
|
||||
### 2.4. JSONPath как единый язык адресации
|
||||
|
||||
Для input и output mapping используется `JSONPath`. Это позволяет единообразно ссылаться на вложенные поля во входе, промежуточном представлении запроса и нормализованном ответе.
|
||||
Для input и output mapping используется `JSONPath`.
|
||||
|
||||
### 2.5. YAML как формат обмена конфигурацией
|
||||
### 2.5. Workspace - обязательная граница данных
|
||||
|
||||
Помимо канонической JSON-модели система должна поддерживать импорт и экспорт конфигураций в `YAML`. Это внешний формат обмена, а не отдельная доменная модель.
|
||||
Все продуктовые сущности принадлежат одному `Workspace`.
|
||||
|
||||
## 3. Корневая сущность `Operation`
|
||||
Минимальный набор workspace-scoped сущностей:
|
||||
|
||||
`Operation` - основная конфигурационная сущность платформы.
|
||||
- `Operation`
|
||||
- `OperationVersion`
|
||||
- `AuthProfile`
|
||||
- `Agent`
|
||||
- `PlatformApiKey`
|
||||
- `InvocationLog`
|
||||
- `UsageRollup`
|
||||
|
||||
### Поля
|
||||
### 2.6. YAML как формат обмена конфигурацией
|
||||
|
||||
- `id` - уникальный идентификатор операции.
|
||||
- `name` - стабильное внутреннее имя.
|
||||
- `display_name` - отображаемое имя в UI.
|
||||
- `protocol` - `rest`, `graphql`, `grpc`.
|
||||
- `status` - `draft`, `testing`, `published`, `archived`.
|
||||
- `version` - версия конфигурации операции.
|
||||
- `target` - описание внешней операции.
|
||||
- `input_schema` - схема MCP-входа.
|
||||
- `output_schema` - схема MCP-выхода.
|
||||
- `input_mapping` - правила подготовки внешнего запроса.
|
||||
- `output_mapping` - правила формирования MCP-ответа.
|
||||
- `execution_config` - auth, headers, timeout, retries и protocol-specific execution settings.
|
||||
- `tool_description` - описание tool для MCP и LLM.
|
||||
- `samples` - загруженные образцы JSON и schema artifacts.
|
||||
- `generated_draft` - автоматически построенный черновик схем и mappings.
|
||||
- `config_export` - опциональные метаданные экспортируемой конфигурации.
|
||||
Помимо канонической JSON-модели система поддерживает импорт и экспорт конфигураций в `YAML`.
|
||||
|
||||
## 3. Корневые сущности
|
||||
|
||||
### 3.1. `Workspace`
|
||||
|
||||
Поля:
|
||||
|
||||
- `id`
|
||||
- `slug`
|
||||
- `display_name`
|
||||
- `status`
|
||||
- `settings`
|
||||
- `created_at`
|
||||
- `updated_at`
|
||||
|
||||
Назначение:
|
||||
|
||||
- логическая изоляция команд;
|
||||
- scoping для операций, агентов, ключей и логов;
|
||||
- основа для multi-tenant MCP endpoints.
|
||||
|
||||
### 3.2. `Operation`
|
||||
|
||||
Поля:
|
||||
|
||||
- `id`
|
||||
- `workspace_id`
|
||||
- `name`
|
||||
- `display_name`
|
||||
- `category`
|
||||
- `protocol`
|
||||
- `status`
|
||||
- `version`
|
||||
- `target`
|
||||
- `input_schema`
|
||||
- `output_schema`
|
||||
- `input_mapping`
|
||||
- `output_mapping`
|
||||
- `execution_config`
|
||||
- `tool_description`
|
||||
- `samples`
|
||||
- `generated_draft`
|
||||
- `config_export`
|
||||
- `created_at`
|
||||
- `updated_at`
|
||||
- `published_at`
|
||||
|
||||
### Пример
|
||||
### 3.3. `Agent`
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "op_01hr7w0m6p8x9z4n7s2k3q5t6u",
|
||||
"name": "crm_create_lead",
|
||||
"display_name": "Create Lead",
|
||||
"protocol": "rest",
|
||||
"status": "draft",
|
||||
"version": 3,
|
||||
"target": {
|
||||
"kind": "rest",
|
||||
"base_url": "https://api.example.com",
|
||||
"method": "POST",
|
||||
"path_template": "/v1/leads"
|
||||
},
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"fields": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"output_schema": {
|
||||
"type": "object",
|
||||
"fields": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"input_mapping": {
|
||||
"rules": [
|
||||
{
|
||||
"source": "$.mcp.name",
|
||||
"target": "$.request.body.name"
|
||||
},
|
||||
{
|
||||
"source": "$.mcp.email",
|
||||
"target": "$.request.body.email"
|
||||
}
|
||||
]
|
||||
},
|
||||
"output_mapping": {
|
||||
"rules": [
|
||||
{
|
||||
"source": "$.response.body.id",
|
||||
"target": "$.output.id"
|
||||
},
|
||||
{
|
||||
"source": "$.response.body.status",
|
||||
"target": "$.output.status"
|
||||
}
|
||||
]
|
||||
},
|
||||
"execution_config": {
|
||||
"timeout_ms": 10000,
|
||||
"auth_profile_ref": "auth_01hr7x8rj2d8nq8v0c4m4t1r9e"
|
||||
},
|
||||
"tool_description": {
|
||||
"title": "Create CRM lead",
|
||||
"description": "Creates a new lead in CRM by name and email."
|
||||
},
|
||||
"samples": {
|
||||
"input_json_sample_ref": "file_01hr7xj7z4k1t0qm0cxsw6f1wx",
|
||||
"output_json_sample_ref": "file_01hr7xkvt3m1p6ms3m7r2dr8wz"
|
||||
},
|
||||
"generated_draft": {
|
||||
"status": "available",
|
||||
"source_types": ["input_json_sample", "output_json_sample"]
|
||||
},
|
||||
"config_export": {
|
||||
"format_version": "1",
|
||||
"export_mode": "portable"
|
||||
},
|
||||
"created_at": "2026-03-25T08:00:00Z",
|
||||
"updated_at": "2026-03-25T08:10:00Z",
|
||||
"published_at": null
|
||||
}
|
||||
```
|
||||
`Agent` - пользовательская MCP-поверхность, которая собирает ограниченный набор published operations.
|
||||
|
||||
Поля:
|
||||
|
||||
- `id`
|
||||
- `workspace_id`
|
||||
- `slug`
|
||||
- `display_name`
|
||||
- `description`
|
||||
- `status`
|
||||
- `current_draft_version`
|
||||
- `latest_published_version`
|
||||
- `created_at`
|
||||
- `updated_at`
|
||||
- `published_at`
|
||||
|
||||
### 3.4. `AgentVersion`
|
||||
|
||||
Снимок конфигурации агента.
|
||||
|
||||
Поля:
|
||||
|
||||
- `agent_id`
|
||||
- `version`
|
||||
- `status`
|
||||
- `instructions`
|
||||
- `tool_selection_policy`
|
||||
- `bindings`
|
||||
- `created_at`
|
||||
|
||||
### 3.5. `AgentOperationBinding`
|
||||
|
||||
Связь published operation с agent version.
|
||||
|
||||
Поля:
|
||||
|
||||
- `operation_id`
|
||||
- `operation_version`
|
||||
- `tool_name`
|
||||
- `tool_title`
|
||||
- `tool_description_override`
|
||||
- `enabled`
|
||||
|
||||
### 3.6. `AuthProfile`
|
||||
|
||||
Используется только для доступа к внешним системам.
|
||||
|
||||
Поля:
|
||||
|
||||
- `id`
|
||||
- `workspace_id`
|
||||
- `name`
|
||||
- `kind`
|
||||
- `config`
|
||||
|
||||
### 3.7. `PlatformApiKey`
|
||||
|
||||
Отдельная сущность для доступа к самой платформе.
|
||||
|
||||
Поля:
|
||||
|
||||
- `id`
|
||||
- `workspace_id`
|
||||
- `name`
|
||||
- `prefix`
|
||||
- `scopes`
|
||||
- `status`
|
||||
- `created_at`
|
||||
- `last_used_at`
|
||||
|
||||
### 3.8. `InvocationLog`
|
||||
|
||||
Продуктовая запись о вызове tool.
|
||||
|
||||
Поля:
|
||||
|
||||
- `id`
|
||||
- `workspace_id`
|
||||
- `agent_id`
|
||||
- `operation_id`
|
||||
- `request_id`
|
||||
- `level`
|
||||
- `status`
|
||||
- `duration_ms`
|
||||
- `error_kind`
|
||||
- `request_preview`
|
||||
- `response_preview`
|
||||
- `created_at`
|
||||
|
||||
### 3.9. `UsageRollup`
|
||||
|
||||
Агрегированная статистика по периоду.
|
||||
|
||||
Поля:
|
||||
|
||||
- `workspace_id`
|
||||
- `agent_id`
|
||||
- `operation_id`
|
||||
- `period_kind`
|
||||
- `period_start`
|
||||
- `calls_total`
|
||||
- `calls_ok`
|
||||
- `calls_error`
|
||||
- `p50_ms`
|
||||
- `p95_ms`
|
||||
- `p99_ms`
|
||||
|
||||
## 4. `Target`
|
||||
|
||||
@@ -162,20 +213,6 @@
|
||||
|
||||
### 4.1. `RestTarget`
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": "rest",
|
||||
"base_url": "https://api.example.com",
|
||||
"method": "PATCH",
|
||||
"path_template": "/v1/users/{userId}",
|
||||
"static_headers": {
|
||||
"X-App-Source": "crank"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Поля:
|
||||
|
||||
- `kind`
|
||||
- `base_url`
|
||||
- `method`
|
||||
@@ -184,19 +221,6 @@
|
||||
|
||||
### 4.2. `GraphqlTarget`
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": "graphql",
|
||||
"endpoint": "https://api.example.com/graphql",
|
||||
"operation_type": "mutation",
|
||||
"operation_name": "CreateLead",
|
||||
"query_template": "mutation CreateLead($input: LeadInput!) { createLead(input: $input) { id status } }",
|
||||
"response_path": "$.response.body.data.createLead"
|
||||
}
|
||||
```
|
||||
|
||||
Поля:
|
||||
|
||||
- `kind`
|
||||
- `endpoint`
|
||||
- `operation_type`
|
||||
@@ -206,20 +230,6 @@
|
||||
|
||||
### 4.3. `GrpcTarget`
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": "grpc",
|
||||
"server_addr": "https://grpc.example.com:443",
|
||||
"package": "crm.v1",
|
||||
"service": "LeadService",
|
||||
"method": "CreateLead",
|
||||
"descriptor_ref": "desc_01hr7yn4d6g1x6vwt7h9n0e7ab",
|
||||
"descriptor_set_b64": "<base64-encoded-descriptor-set>"
|
||||
}
|
||||
```
|
||||
|
||||
Поля:
|
||||
|
||||
- `kind`
|
||||
- `server_addr`
|
||||
- `package`
|
||||
@@ -228,499 +238,23 @@
|
||||
- `descriptor_ref`
|
||||
- `descriptor_set_b64`
|
||||
|
||||
`descriptor_ref` остается ссылкой на загруженный descriptor artifact в storage и registry.
|
||||
|
||||
`descriptor_set_b64` - runtime-ready snapshot descriptor set, который используется unary gRPC adapter для динамического вызова метода без генерации Rust-кода.
|
||||
|
||||
## 5. `Schema`
|
||||
|
||||
`Schema` - нормализованное описание входа или выхода. Это не JSON Schema в полном объеме, а внутренняя структурная модель, удобная для UI и runtime.
|
||||
`Schema` - нормализованное описание входа или выхода.
|
||||
|
||||
### Базовая форма
|
||||
Поддерживаются:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"description": "Lead input",
|
||||
"fields": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "Lead full name"
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"required": false,
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
- скалярные поля;
|
||||
- вложенные объекты;
|
||||
- массивы;
|
||||
- enum;
|
||||
- nullable-поля;
|
||||
- `oneof` для protobuf.
|
||||
|
||||
### Поддерживаемые типы
|
||||
## 6. Принцип совместимости
|
||||
|
||||
- `object`
|
||||
- `array`
|
||||
- `string`
|
||||
- `integer`
|
||||
- `number`
|
||||
- `boolean`
|
||||
- `enum`
|
||||
- `null`
|
||||
- `oneof`
|
||||
Если UI требует сущность, которой нет в текущем backend, эта сущность должна быть сначала явно добавлена в эту модель данных, а уже потом в код и БД.
|
||||
|
||||
### Модель поля
|
||||
Для `Operations` и `Wizard` дополнительный уровень контрактной детализации закреплен в:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"nullable": false,
|
||||
"description": "User email",
|
||||
"default": null
|
||||
}
|
||||
```
|
||||
|
||||
Тип объекта:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"required": true,
|
||||
"fields": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Тип массива:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "array",
|
||||
"required": false,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"fields": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Нормализация protobuf-структур
|
||||
|
||||
Для protobuf -> schema bridge дополнительно фиксируются такие правила:
|
||||
|
||||
- `repeated` поле преобразуется в `array`;
|
||||
- `enum` преобразуется в `type: enum` со списком `enum_values`;
|
||||
- `map<K, V>` преобразуется в `array` объектов `{ key, value }`;
|
||||
- `oneof` преобразуется в `type: oneof`, где каждый вариант представлен объектом с одним допустимым полем.
|
||||
|
||||
## 6. `MappingSet` и `MappingRule`
|
||||
|
||||
`MappingSet` - набор правил преобразования между внутренним MCP input/output и protocol-specific request/response model.
|
||||
|
||||
### `MappingSet`
|
||||
|
||||
```json
|
||||
{
|
||||
"rules": [
|
||||
{
|
||||
"source": "$.mcp.user_id",
|
||||
"target": "$.request.path.userId"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### `MappingRule`
|
||||
|
||||
Поля:
|
||||
|
||||
- `source` - `JSONPath` в исходном контексте.
|
||||
- `target` - `JSONPath` в целевом контексте.
|
||||
- `required` - обязательно ли правило для корректного вызова.
|
||||
- `default_value` - значение по умолчанию.
|
||||
- `transform` - встроенное преобразование.
|
||||
- `condition` - условие применения правила.
|
||||
- `notes` - служебное описание для UI.
|
||||
|
||||
Пример:
|
||||
|
||||
```json
|
||||
{
|
||||
"source": "$.mcp.profile.email",
|
||||
"target": "$.request.body.contact.email",
|
||||
"required": true,
|
||||
"default_value": null,
|
||||
"transform": {
|
||||
"kind": "identity"
|
||||
},
|
||||
"condition": null,
|
||||
"notes": "Map email to CRM contact payload"
|
||||
}
|
||||
```
|
||||
|
||||
### Контексты `source` и `target`
|
||||
|
||||
Для input mapping:
|
||||
|
||||
- `$.mcp.*`
|
||||
- `$.request.path.*`
|
||||
- `$.request.query.*`
|
||||
- `$.request.headers.*`
|
||||
- `$.request.body.*`
|
||||
- `$.request.variables.*`
|
||||
- `$.request.grpc.*`
|
||||
|
||||
Для output mapping:
|
||||
|
||||
- `$.response.body.*`
|
||||
- `$.response.data.*`
|
||||
- `$.response.grpc.*`
|
||||
- `$.output.*`
|
||||
|
||||
Для MVP достаточно управляемого подмножества `JSONPath`:
|
||||
|
||||
- путь всегда начинается с фиксированного root context;
|
||||
- дальше используются dot-separated поля;
|
||||
- для массивов допускаются numeric indexes вида `[0]`;
|
||||
- quoted selectors, filter expressions и произвольные функции не поддерживаются.
|
||||
|
||||
### `Transform`
|
||||
|
||||
Для MVP transformations должны быть ограничены:
|
||||
|
||||
- `identity`
|
||||
- `to_string`
|
||||
- `to_number`
|
||||
- `to_boolean`
|
||||
- `join`
|
||||
- `split`
|
||||
- `wrap_array`
|
||||
- `unwrap_singleton`
|
||||
|
||||
Пример:
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": "to_string"
|
||||
}
|
||||
```
|
||||
|
||||
### Черновая генерация mapping
|
||||
|
||||
Для MVP generation draft mapping может опираться на простое правило:
|
||||
|
||||
- система сопоставляет уникальные leaf-поля с одинаковыми именами в source sample и target sample;
|
||||
- неоднозначные совпадения автоматически не связываются;
|
||||
- результат всегда остается черновиком и требует ручной проверки оператором.
|
||||
|
||||
## 7. `ExecutionConfig`
|
||||
|
||||
`ExecutionConfig` задает параметры выполнения operation.
|
||||
|
||||
```json
|
||||
{
|
||||
"timeout_ms": 10000,
|
||||
"retry_policy": {
|
||||
"max_attempts": 1
|
||||
},
|
||||
"auth_profile_ref": "auth_01hr7x8rj2d8nq8v0c4m4t1r9e",
|
||||
"headers": {
|
||||
"X-Client": "crank"
|
||||
},
|
||||
"protocol_options": {
|
||||
"rest": null,
|
||||
"graphql": null,
|
||||
"grpc": {
|
||||
"use_tls": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Поля:
|
||||
|
||||
- `timeout_ms`
|
||||
- `retry_policy`
|
||||
- `auth_profile_ref`
|
||||
- `headers`
|
||||
- `protocol_options`
|
||||
|
||||
Важно:
|
||||
|
||||
- здесь хранятся execution settings, а не описание бизнес-схемы;
|
||||
- `protocol_options` должны оставаться узкими и протокол-специфичными;
|
||||
- секреты не хранятся внутри operation, только ссылки на secret store или auth profile.
|
||||
|
||||
## 8. `AuthProfile`
|
||||
|
||||
`AuthProfile` - переиспользуемая конфигурация аутентификации для внешних вызовов.
|
||||
|
||||
Operation ссылается на auth profile через `auth_profile_ref`, а не хранит секреты внутри себя.
|
||||
|
||||
### Базовая форма
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "auth_01hr7x8rj2d8nq8v0c4m4t1r9e",
|
||||
"name": "crm-prod-bearer",
|
||||
"kind": "bearer",
|
||||
"config": {
|
||||
"header_name": "Authorization",
|
||||
"secret_ref": "secret://auth/crm-prod-token"
|
||||
},
|
||||
"created_at": "2026-03-25T08:00:00Z",
|
||||
"updated_at": "2026-03-25T08:10:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Поддерживаемые виды
|
||||
|
||||
- `bearer`
|
||||
- `basic`
|
||||
- `api_key_header`
|
||||
- `api_key_query`
|
||||
|
||||
### MVP-решение по секретам
|
||||
|
||||
Для MVP секреты должны храниться не в open text внутри operation version, а в отдельном secret storage слое.
|
||||
|
||||
Рекомендуемое решение:
|
||||
|
||||
- логическая ссылка в формате `secret://...`;
|
||||
- реальное значение хранится в приложении либо в зашифрованном хранилище, либо в env-backed secret store;
|
||||
- в документации и YAML export секреты всегда представляются только через `secret_ref`.
|
||||
|
||||
## 9. `ToolDescription`
|
||||
|
||||
`ToolDescription` задает MCP-представление operation.
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Get customer profile",
|
||||
"description": "Returns a customer profile by external customer identifier.",
|
||||
"tags": ["crm", "customer"],
|
||||
"examples": [
|
||||
{
|
||||
"input": {
|
||||
"customer_id": "123"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Поля:
|
||||
|
||||
- `title`
|
||||
- `description`
|
||||
- `tags`
|
||||
- `examples`
|
||||
|
||||
## 10. `Samples`
|
||||
|
||||
`Samples` связывает operation с загруженными артефактами, на основе которых может быть построен черновик.
|
||||
|
||||
```json
|
||||
{
|
||||
"input_json_sample_ref": "file_01hr7xj7z4k1t0qm0cxsw6f1wx",
|
||||
"output_json_sample_ref": "file_01hr7xkvt3m1p6ms3m7r2dr8wz",
|
||||
"proto_file_ref": null,
|
||||
"descriptor_ref": null
|
||||
}
|
||||
```
|
||||
|
||||
Поля:
|
||||
|
||||
- `input_json_sample_ref`
|
||||
- `output_json_sample_ref`
|
||||
- `proto_file_ref`
|
||||
- `descriptor_ref`
|
||||
|
||||
Примечания:
|
||||
|
||||
- для REST чаще всего используются входной и выходной JSON samples;
|
||||
- для GraphQL чаще всего полезен sample ответа;
|
||||
- для gRPC основным artifact остается `.proto` или descriptor set, но input/output JSON samples тоже могут использоваться для MCP-facing модели.
|
||||
|
||||
## 11. `GeneratedDraft`
|
||||
|
||||
`GeneratedDraft` хранит результат автоматической генерации схем и mappings.
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "available",
|
||||
"source_types": ["input_json_sample", "output_json_sample"],
|
||||
"generated_at": "2026-03-25T08:05:00Z",
|
||||
"input_schema_generated": true,
|
||||
"output_schema_generated": true,
|
||||
"input_mapping_generated": true,
|
||||
"output_mapping_generated": true,
|
||||
"warnings": [
|
||||
"Field $.response.body.meta was not mapped automatically"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Поля:
|
||||
|
||||
- `status` - `none`, `available`, `stale`, `failed`
|
||||
- `source_types`
|
||||
- `generated_at`
|
||||
- `input_schema_generated`
|
||||
- `output_schema_generated`
|
||||
- `input_mapping_generated`
|
||||
- `output_mapping_generated`
|
||||
- `warnings`
|
||||
|
||||
Важно:
|
||||
|
||||
- generated draft - это не runtime-источник истины;
|
||||
- runtime использует только сохраненные `input_schema`, `output_schema`, `input_mapping`, `output_mapping`;
|
||||
- generated draft нужен как вспомогательный слой для UI и ускорения конфигурирования.
|
||||
|
||||
## 12. Runtime view
|
||||
|
||||
Для исполнения operation должно существовать упрощенное runtime-представление без UI-специфики.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "op_01hr7w0m6p8x9z4n7s2k3q5t6u",
|
||||
"protocol": "rest",
|
||||
"target": {
|
||||
"kind": "rest",
|
||||
"base_url": "https://api.example.com",
|
||||
"method": "POST",
|
||||
"path_template": "/v1/leads"
|
||||
},
|
||||
"input_schema": { "type": "object", "fields": {} },
|
||||
"output_schema": { "type": "object", "fields": {} },
|
||||
"input_mapping": { "rules": [] },
|
||||
"output_mapping": { "rules": [] },
|
||||
"execution_config": {
|
||||
"timeout_ms": 10000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Runtime view должно:
|
||||
|
||||
- не содержать UI draft metadata;
|
||||
- не зависеть от raw uploaded files;
|
||||
- быть готовым к немедленному исполнению адаптером.
|
||||
|
||||
## 13. YAML-конфигурация
|
||||
|
||||
Для импорта и экспорта система должна поддерживать `YAML`-представление operation.
|
||||
|
||||
Принцип:
|
||||
|
||||
- внутренняя доменная модель одна;
|
||||
- `JSON` и `YAML` - это два способа сериализации одной и той же конфигурации;
|
||||
- runtime не зависит от конкретного формата файла;
|
||||
- `YAML` нужен для переносимости и ручного редактирования.
|
||||
|
||||
### Базовая структура YAML
|
||||
|
||||
```yaml
|
||||
format_version: "1"
|
||||
kind: operation
|
||||
operation:
|
||||
id: op_01hr7w0m6p8x9z4n7s2k3q5t6u
|
||||
name: crm_create_lead
|
||||
display_name: Create Lead
|
||||
protocol: rest
|
||||
status: draft
|
||||
version: 3
|
||||
target:
|
||||
kind: rest
|
||||
base_url: https://api.example.com
|
||||
method: POST
|
||||
path_template: /v1/leads
|
||||
input_schema:
|
||||
type: object
|
||||
fields:
|
||||
name:
|
||||
type: string
|
||||
required: true
|
||||
email:
|
||||
type: string
|
||||
required: true
|
||||
output_schema:
|
||||
type: object
|
||||
fields:
|
||||
id:
|
||||
type: string
|
||||
required: true
|
||||
status:
|
||||
type: string
|
||||
required: true
|
||||
input_mapping:
|
||||
rules:
|
||||
- source: $.mcp.name
|
||||
target: $.request.body.name
|
||||
- source: $.mcp.email
|
||||
target: $.request.body.email
|
||||
output_mapping:
|
||||
rules:
|
||||
- source: $.response.body.id
|
||||
target: $.output.id
|
||||
- source: $.response.body.status
|
||||
target: $.output.status
|
||||
execution_config:
|
||||
timeout_ms: 10000
|
||||
auth_profile_ref: auth_01hr7x8rj2d8nq8v0c4m4t1r9e
|
||||
tool_description:
|
||||
title: Create CRM lead
|
||||
description: Creates a new lead in CRM by name and email.
|
||||
```
|
||||
|
||||
### Требования к YAML import/export
|
||||
|
||||
- формат должен быть детерминированным;
|
||||
- структура должна быть человекочитаемой;
|
||||
- импорт должен валидировать схему, mapping и protocol-specific target;
|
||||
- экспорт не должен включать секреты в открытом виде;
|
||||
- ссылки на внешние артефакты допустимы, но режим экспорта должен быть явным.
|
||||
|
||||
### Режимы экспорта
|
||||
|
||||
Минимально стоит предусмотреть два режима:
|
||||
|
||||
- `portable` - экспорт только конфигурации operation и ссылок на внешние артефакты;
|
||||
- `bundle` - экспорт конфигурации operation вместе с вложенными sample metadata и descriptor metadata, если это допустимо.
|
||||
|
||||
Для MVP можно начать только с `portable`.
|
||||
|
||||
## 14. Что важно не допустить
|
||||
|
||||
- одну гигантскую `Operation`, в которой protocol-specific поля лежат вперемешку;
|
||||
- неявный mapping, который не сохраняется после генерации черновика;
|
||||
- смешивание uploaded artifacts и runtime-ready configuration;
|
||||
- хранение секретов внутри operation;
|
||||
- хранение реальных auth credentials внутри YAML export;
|
||||
- произвольные пользовательские скрипты в mapping;
|
||||
- YAML-экспорт, который становится отдельной несовместимой моделью по отношению к доменной структуре.
|
||||
|
||||
## 15. Практический итог
|
||||
|
||||
Эта модель задает основу для:
|
||||
|
||||
- Rust structs в `crank-core`, `crank-schema`, `crank-mapping`;
|
||||
- DTO для `admin-api`;
|
||||
- таблиц `operations`, `operation_versions`, `operation_samples`, `operation_descriptors`;
|
||||
- import/export layer для `YAML` конфигураций;
|
||||
- runtime view, который будет передаваться в `crank-runtime`.
|
||||
|
||||
Следующим логическим шагом после этого документа должна стать схема БД, в которой эти сущности будут разложены по таблицам и связям.
|
||||
- `docs/operations-workspace-contracts.md`
|
||||
|
||||
+259
-335
@@ -2,364 +2,288 @@
|
||||
|
||||
## 1. Назначение документа
|
||||
|
||||
Этот документ фиксирует структуру хранения конфигураций, версий операций, загруженных артефактов и published runtime-view. Его цель - дать основу для SQL-миграций и для реализации `crank-registry`.
|
||||
|
||||
В документе предполагается реляционная модель, ориентированная на `PostgreSQL`. Канонической считается схема, совместимая с `PostgreSQL`.
|
||||
Этот документ фиксирует целевую структуру хранения workspace-scoped конфигураций, агентов, ключей доступа и observability-данных. Базовая СУБД - `PostgreSQL`.
|
||||
|
||||
## 2. Общие принципы хранения
|
||||
|
||||
### 2.1. Версионирование обязательно
|
||||
|
||||
Конфигурация operation не должна храниться только в одной "живой" записи. Каждое существенное изменение должно приводить к появлению новой версии конфигурации.
|
||||
|
||||
Для MVP в registry version snapshot хранит protocol-specific конфигурацию, схемы, mapping и execution settings. Поля identity и listing view (`name`, `display_name`, `protocol`) считаются стабильными и хранятся в `operations`.
|
||||
Конфигурация operation и agent не хранится только в одной "живой" записи. Каждое существенное изменение создает новую версию.
|
||||
|
||||
### 2.2. Published и draft разделяются логически
|
||||
|
||||
- `draft` может меняться;
|
||||
- `published` должна ссылаться на конкретную зафиксированную версию;
|
||||
- runtime читает только опубликованные версии.
|
||||
- `published` всегда указывает на конкретную version;
|
||||
- runtime читает только опубликованные представления.
|
||||
|
||||
### 2.3. Артефакты и конфигурация не смешиваются
|
||||
### 2.3. Workspace scoping обязателен
|
||||
|
||||
`.proto`, descriptor set, sample JSON и YAML import payload не должны храниться в той же структуре, что и runtime-ready configuration.
|
||||
Все продуктовые таблицы должны ссылаться на `workspaces`.
|
||||
|
||||
### 2.4. Секреты не хранятся внутри operation
|
||||
### 2.4. Артефакты и конфигурация не смешиваются
|
||||
|
||||
В БД operation должны храниться только ссылки на auth profiles или secret references.
|
||||
`.proto`, descriptor set, sample JSON и YAML payload не хранятся в тех же строках, что runtime-ready configuration.
|
||||
|
||||
Для MVP рекомендуется отдельная таблица `auth_profiles`, где metadata и secret refs отделены от operation versions.
|
||||
### 2.5. Секреты не хранятся в открытом виде
|
||||
|
||||
### 2.5. Тестовая изоляция
|
||||
|
||||
Integration tests для registry должны выполняться на реальной `PostgreSQL`, но без влияния на runtime-данные. Предпочтительный способ:
|
||||
|
||||
- отдельная test database;
|
||||
- либо отдельная временная schema на время теста;
|
||||
- обязательная очистка после завершения тестов.
|
||||
- upstream secrets живут за `secret_ref`;
|
||||
- platform API keys хранятся как hash.
|
||||
|
||||
## 3. Основные таблицы
|
||||
|
||||
Минимальный набор таблиц:
|
||||
|
||||
- `workspaces`
|
||||
- `users`
|
||||
- `memberships`
|
||||
- `invitation_tokens`
|
||||
- `operations`
|
||||
- `operation_versions`
|
||||
- `published_operations`
|
||||
- `operation_samples`
|
||||
- `descriptors`
|
||||
- `auth_profiles`
|
||||
- `agents`
|
||||
- `agent_versions`
|
||||
- `agent_operation_bindings`
|
||||
- `published_agents`
|
||||
- `platform_api_keys`
|
||||
- `invocation_logs`
|
||||
- `usage_rollups`
|
||||
- `yaml_import_jobs`
|
||||
|
||||
Опционально позже:
|
||||
|
||||
- `operation_test_runs`
|
||||
- `audit_log`
|
||||
|
||||
## 4. Таблица `operations`
|
||||
|
||||
Хранит стабильную сущность операции, не зависящую от конкретной версии.
|
||||
|
||||
### Поля
|
||||
|
||||
- `id` `text primary key`
|
||||
- `name` `text not null unique`
|
||||
- `display_name` `text not null`
|
||||
- `protocol` `text not null`
|
||||
- `status` `text not null`
|
||||
- `current_draft_version` `integer not null default 1`
|
||||
- `latest_published_version` `integer null`
|
||||
- `created_at` `timestamptz not null`
|
||||
- `updated_at` `timestamptz not null`
|
||||
- `published_at` `timestamptz null`
|
||||
|
||||
### Назначение
|
||||
|
||||
- быстрый список операций;
|
||||
- стабильный идентификатор для UI и MCP;
|
||||
- привязка к актуальному draft и опубликованной версии.
|
||||
|
||||
## 5. Таблица `operation_versions`
|
||||
|
||||
Хранит полную сериализованную конфигурацию конкретной версии operation.
|
||||
|
||||
### Поля
|
||||
|
||||
- `operation_id` `text not null`
|
||||
- `version` `integer not null`
|
||||
- `status` `text not null`
|
||||
- `target_json` `jsonb not null`
|
||||
- `input_schema_json` `jsonb not null`
|
||||
- `output_schema_json` `jsonb not null`
|
||||
- `input_mapping_json` `jsonb not null`
|
||||
- `output_mapping_json` `jsonb not null`
|
||||
- `execution_config_json` `jsonb not null`
|
||||
- `tool_description_json` `jsonb not null`
|
||||
- `samples_json` `jsonb null`
|
||||
- `generated_draft_json` `jsonb null`
|
||||
- `config_export_json` `jsonb null`
|
||||
- `change_note` `text null`
|
||||
- `created_at` `timestamptz not null`
|
||||
- `created_by` `text null`
|
||||
|
||||
### Ключи
|
||||
|
||||
- primary key: `(operation_id, version)`
|
||||
- foreign key: `operation_id -> operations(id)`
|
||||
- рекомендованный composite foreign key для связанных таблиц: `(operation_id, version)`
|
||||
|
||||
### Почему так
|
||||
|
||||
Для MVP выгоднее хранить version snapshot целиком, а не дробить по десятку связанных таблиц. Это:
|
||||
|
||||
- упрощает versioning;
|
||||
- упрощает откат;
|
||||
- упрощает YAML export;
|
||||
- хорошо сочетается с JSON-oriented доменной моделью.
|
||||
|
||||
## 6. Таблица `published_operations`
|
||||
|
||||
Хранит явную published-привязку, которую читает runtime.
|
||||
|
||||
### Поля
|
||||
|
||||
- `operation_id` `text primary key`
|
||||
- `version` `integer not null`
|
||||
- `published_at` `timestamptz not null`
|
||||
- `published_by` `text null`
|
||||
|
||||
### Назначение
|
||||
|
||||
- быстрый доступ к published runtime-view;
|
||||
- отсутствие двусмысленности, какая именно версия сейчас активна;
|
||||
- простой invalidation для runtime cache.
|
||||
|
||||
### Рекомендуемая целостность
|
||||
|
||||
- `operation_id -> operations(id)`
|
||||
- `(operation_id, version) -> operation_versions(operation_id, version)`
|
||||
|
||||
## 7. Таблица `operation_samples`
|
||||
|
||||
Хранит метаданные и ссылки на sample artifacts.
|
||||
|
||||
### Поля
|
||||
|
||||
- `id` `text primary key`
|
||||
- `operation_id` `text not null`
|
||||
- `version` `integer not null`
|
||||
- `sample_kind` `text not null`
|
||||
- `storage_ref` `text not null`
|
||||
- `content_type` `text not null`
|
||||
- `file_name` `text null`
|
||||
- `created_at` `timestamptz not null`
|
||||
|
||||
### Варианты `sample_kind`
|
||||
|
||||
- `input_json`
|
||||
- `output_json`
|
||||
- `yaml_import_source`
|
||||
|
||||
### Назначение
|
||||
|
||||
- не класть большие sample payload в основные version records;
|
||||
- иметь возможность переиспользовать или пересобирать draft mapping;
|
||||
- отслеживать, из каких sample-данных строился черновик.
|
||||
|
||||
### Рекомендуемая целостность
|
||||
|
||||
- `operation_id -> operations(id)`
|
||||
- `(operation_id, version) -> operation_versions(operation_id, version)`
|
||||
|
||||
## 8. Таблица `descriptors`
|
||||
|
||||
Хранит gRPC schema artifacts.
|
||||
|
||||
### Поля
|
||||
|
||||
- `id` `text primary key`
|
||||
- `operation_id` `text null`
|
||||
- `version` `integer null`
|
||||
- `descriptor_kind` `text not null`
|
||||
- `storage_ref` `text not null`
|
||||
- `source_name` `text null`
|
||||
- `package_index_json` `jsonb null`
|
||||
- `created_at` `timestamptz not null`
|
||||
|
||||
### Варианты `descriptor_kind`
|
||||
|
||||
- `proto_upload`
|
||||
- `descriptor_set`
|
||||
- `reflection_snapshot`
|
||||
|
||||
### Назначение
|
||||
|
||||
- связывать gRPC operation с конкретной схемой;
|
||||
- не хранить binary descriptor внутри основной operation version;
|
||||
- иметь отдельную точку для discovery metadata.
|
||||
|
||||
### Рекомендуемая целостность
|
||||
|
||||
- если descriptor привязан к version, то `(operation_id, version) -> operation_versions(operation_id, version)`
|
||||
|
||||
## 9. Таблица `yaml_import_jobs`
|
||||
|
||||
Для MVP можно импортировать YAML синхронно, но таблицу под журнал импорта лучше предусмотреть сразу.
|
||||
|
||||
### Поля
|
||||
|
||||
- `id` `text primary key`
|
||||
- `source_sample_id` `text null`
|
||||
- `status` `text not null`
|
||||
- `format_version` `text not null`
|
||||
- `mode` `text not null`
|
||||
- `result_operation_id` `text null`
|
||||
- `result_version` `integer null`
|
||||
- `error_text` `text null`
|
||||
- `created_at` `timestamptz not null`
|
||||
- `finished_at` `timestamptz null`
|
||||
|
||||
### Назначение
|
||||
|
||||
- аудит импортов;
|
||||
- разбор ошибок валидации;
|
||||
- поддержка будущего async import pipeline.
|
||||
|
||||
## 10. Таблица `auth_profiles`
|
||||
|
||||
Хранит переиспользуемые профили аутентификации для внешних вызовов.
|
||||
|
||||
### Поля
|
||||
|
||||
- `id` `text primary key`
|
||||
- `name` `text not null unique`
|
||||
- `kind` `text not null`
|
||||
- `config_json` `jsonb not null`
|
||||
- `created_at` `timestamptz not null`
|
||||
- `updated_at` `timestamptz not null`
|
||||
|
||||
### Варианты `kind`
|
||||
|
||||
- `bearer`
|
||||
- `basic`
|
||||
- `api_key_header`
|
||||
- `api_key_query`
|
||||
|
||||
### Правило
|
||||
|
||||
`config_json` должен содержать только `secret_ref`, а не открытые секреты.
|
||||
|
||||
## 11. Предлагаемая SQL-форма
|
||||
|
||||
```sql
|
||||
create table operations (
|
||||
id text primary key,
|
||||
name text not null unique,
|
||||
display_name text not null,
|
||||
protocol text not null,
|
||||
status text not null,
|
||||
current_draft_version integer not null default 1,
|
||||
latest_published_version integer null,
|
||||
created_at timestamptz not null,
|
||||
updated_at timestamptz not null,
|
||||
published_at timestamptz null
|
||||
);
|
||||
|
||||
create table operation_versions (
|
||||
operation_id text not null references operations(id),
|
||||
version integer not null,
|
||||
status text not null,
|
||||
target_json jsonb not null,
|
||||
input_schema_json jsonb not null,
|
||||
output_schema_json jsonb not null,
|
||||
input_mapping_json jsonb not null,
|
||||
output_mapping_json jsonb not null,
|
||||
execution_config_json jsonb not null,
|
||||
tool_description_json jsonb not null,
|
||||
samples_json jsonb null,
|
||||
generated_draft_json jsonb null,
|
||||
config_export_json jsonb null,
|
||||
change_note text null,
|
||||
created_at timestamptz not null,
|
||||
created_by text null,
|
||||
primary key (operation_id, version)
|
||||
);
|
||||
|
||||
create table published_operations (
|
||||
operation_id text primary key references operations(id),
|
||||
version integer not null,
|
||||
published_at timestamptz not null,
|
||||
published_by text null,
|
||||
foreign key (operation_id, version)
|
||||
references operation_versions(operation_id, version)
|
||||
);
|
||||
|
||||
create table auth_profiles (
|
||||
id text primary key,
|
||||
name text not null unique,
|
||||
kind text not null,
|
||||
config_json jsonb not null,
|
||||
created_at timestamptz not null,
|
||||
updated_at timestamptz not null
|
||||
);
|
||||
```
|
||||
|
||||
## 12. Индексы
|
||||
|
||||
Минимально нужны:
|
||||
|
||||
- index on `operations(protocol)`
|
||||
- index on `operations(status)`
|
||||
- index on `operation_versions(operation_id, created_at desc)`
|
||||
- index on `published_operations(version)`
|
||||
- index on `operation_samples(operation_id, version)`
|
||||
- index on `descriptors(operation_id, version)`
|
||||
- index on `auth_profiles(kind)`
|
||||
|
||||
## 13. Versioning flow
|
||||
|
||||
### Создание операции
|
||||
|
||||
1. Создается запись в `operations`.
|
||||
2. Создается версия `1` в `operation_versions`.
|
||||
3. `current_draft_version = 1`.
|
||||
|
||||
### Изменение draft
|
||||
|
||||
1. Читается текущий draft.
|
||||
2. Создается новая версия `n + 1`.
|
||||
3. В `operations.current_draft_version` пишется новая версия.
|
||||
4. Published версия не меняется.
|
||||
|
||||
### Публикация
|
||||
|
||||
1. Берется текущий draft version.
|
||||
2. В `published_operations` upsert-ится ссылка на эту версию.
|
||||
3. В `operations.latest_published_version` пишется та же версия.
|
||||
4. Runtime cache получает сигнал на reload.
|
||||
|
||||
### Импорт YAML
|
||||
|
||||
1. YAML валидируется.
|
||||
2. Определяется create или update сценарий.
|
||||
3. Создается новая запись в `operation_versions`.
|
||||
4. При необходимости создается запись в `yaml_import_jobs`.
|
||||
|
||||
## 14. Что не должно храниться в БД в таком виде
|
||||
|
||||
- секреты в открытом виде;
|
||||
- runtime cache;
|
||||
- скомпилированные adapter clients;
|
||||
- невалидированные черновики, не приводимые к доменной модели.
|
||||
|
||||
## 15. Практический итог
|
||||
|
||||
Для MVP рекомендован такой подход:
|
||||
|
||||
- `operations` - стабильная идентичность;
|
||||
- `operation_versions` - полные version snapshots;
|
||||
- `published_operations` - текущая активная версия;
|
||||
- `operation_samples` и `descriptors` - внешние артефакты;
|
||||
- `auth_profiles` - переиспользуемая внешняя аутентификация;
|
||||
- `yaml_import_jobs` - журнал импортов.
|
||||
|
||||
Эта схема хорошо ложится на `sqlx`, не требует избыточной нормализации и соответствует JSON-oriented модели домена.
|
||||
## 4. Operations
|
||||
|
||||
### `operations`
|
||||
|
||||
- `id`
|
||||
- `workspace_id`
|
||||
- `name`
|
||||
- `display_name`
|
||||
- `protocol`
|
||||
- `status`
|
||||
- `current_draft_version`
|
||||
- `latest_published_version`
|
||||
- `created_at`
|
||||
- `updated_at`
|
||||
- `published_at`
|
||||
|
||||
Ограничение:
|
||||
|
||||
- `unique (workspace_id, name)`
|
||||
|
||||
### `operation_versions`
|
||||
|
||||
- `operation_id`
|
||||
- `version`
|
||||
- `status`
|
||||
- `target_json`
|
||||
- `input_schema_json`
|
||||
- `output_schema_json`
|
||||
- `input_mapping_json`
|
||||
- `output_mapping_json`
|
||||
- `execution_config_json`
|
||||
- `tool_description_json`
|
||||
- `samples_json`
|
||||
- `generated_draft_json`
|
||||
- `config_export_json`
|
||||
- `change_note`
|
||||
- `created_at`
|
||||
- `created_by`
|
||||
|
||||
### `published_operations`
|
||||
|
||||
- `operation_id`
|
||||
- `version`
|
||||
- `published_at`
|
||||
- `published_by`
|
||||
|
||||
## 5. Operation artifacts
|
||||
|
||||
### `operation_samples`
|
||||
|
||||
- `id`
|
||||
- `operation_id`
|
||||
- `version`
|
||||
- `sample_kind`
|
||||
- `storage_ref`
|
||||
- `content_type`
|
||||
- `file_name`
|
||||
- `created_at`
|
||||
|
||||
### `descriptors`
|
||||
|
||||
- `id`
|
||||
- `operation_id`
|
||||
- `version`
|
||||
- `descriptor_kind`
|
||||
- `storage_ref`
|
||||
- `source_name`
|
||||
- `package_index_json`
|
||||
- `created_at`
|
||||
|
||||
### `yaml_import_jobs`
|
||||
|
||||
- `id`
|
||||
- `source_sample_id`
|
||||
- `status`
|
||||
- `format_version`
|
||||
- `mode`
|
||||
- `result_operation_id`
|
||||
- `result_version`
|
||||
- `error_text`
|
||||
- `created_at`
|
||||
- `finished_at`
|
||||
|
||||
## 6. Upstream auth
|
||||
|
||||
### `auth_profiles`
|
||||
|
||||
- `id`
|
||||
- `workspace_id`
|
||||
- `name`
|
||||
- `kind`
|
||||
- `config_json`
|
||||
- `created_at`
|
||||
- `updated_at`
|
||||
|
||||
Ограничение:
|
||||
|
||||
- `unique (workspace_id, name)`
|
||||
|
||||
## 7. Workspaces and access layer
|
||||
|
||||
### `workspaces`
|
||||
|
||||
- `id`
|
||||
- `slug`
|
||||
- `display_name`
|
||||
- `status`
|
||||
- `settings_json`
|
||||
- `created_at`
|
||||
- `updated_at`
|
||||
|
||||
### `users`
|
||||
|
||||
- `id`
|
||||
- `email`
|
||||
- `display_name`
|
||||
- `status`
|
||||
- `created_at`
|
||||
|
||||
### `memberships`
|
||||
|
||||
- `workspace_id`
|
||||
- `user_id`
|
||||
- `role`
|
||||
- `created_at`
|
||||
|
||||
### `invitation_tokens`
|
||||
|
||||
- `id`
|
||||
- `workspace_id`
|
||||
- `email`
|
||||
- `role`
|
||||
- `status`
|
||||
- `token_hash`
|
||||
- `expires_at`
|
||||
- `created_at`
|
||||
|
||||
## 8. Agents
|
||||
|
||||
### `agents`
|
||||
|
||||
- `id`
|
||||
- `workspace_id`
|
||||
- `slug`
|
||||
- `display_name`
|
||||
- `description`
|
||||
- `status`
|
||||
- `current_draft_version`
|
||||
- `latest_published_version`
|
||||
- `created_at`
|
||||
- `updated_at`
|
||||
- `published_at`
|
||||
|
||||
Ограничение:
|
||||
|
||||
- `unique (workspace_id, slug)`
|
||||
|
||||
### `agent_versions`
|
||||
|
||||
- `agent_id`
|
||||
- `version`
|
||||
- `status`
|
||||
- `instructions_json`
|
||||
- `tool_selection_policy_json`
|
||||
- `created_at`
|
||||
|
||||
### `agent_operation_bindings`
|
||||
|
||||
- `agent_id`
|
||||
- `agent_version`
|
||||
- `operation_id`
|
||||
- `operation_version`
|
||||
- `tool_name`
|
||||
- `tool_title`
|
||||
- `tool_description_override`
|
||||
- `enabled`
|
||||
|
||||
### `published_agents`
|
||||
|
||||
- `agent_id`
|
||||
- `version`
|
||||
- `published_at`
|
||||
- `published_by`
|
||||
|
||||
## 9. Platform access and observability
|
||||
|
||||
### `platform_api_keys`
|
||||
|
||||
- `id`
|
||||
- `workspace_id`
|
||||
- `name`
|
||||
- `prefix`
|
||||
- `secret_hash`
|
||||
- `scopes_json`
|
||||
- `status`
|
||||
- `created_at`
|
||||
- `last_used_at`
|
||||
|
||||
### `invocation_logs`
|
||||
|
||||
- `id`
|
||||
- `workspace_id`
|
||||
- `agent_id`
|
||||
- `operation_id`
|
||||
- `request_id`
|
||||
- `level`
|
||||
- `status`
|
||||
- `duration_ms`
|
||||
- `error_kind`
|
||||
- `request_preview_json`
|
||||
- `response_preview_json`
|
||||
- `created_at`
|
||||
|
||||
### `usage_rollups`
|
||||
|
||||
- `workspace_id`
|
||||
- `agent_id`
|
||||
- `operation_id`
|
||||
- `period_kind`
|
||||
- `period_start`
|
||||
- `calls_total`
|
||||
- `calls_ok`
|
||||
- `calls_error`
|
||||
- `p50_ms`
|
||||
- `p95_ms`
|
||||
- `p99_ms`
|
||||
|
||||
## 10. Migration strategy
|
||||
|
||||
Переход от текущей схемы к целевой идет так:
|
||||
|
||||
1. добавить `workspaces` и заполнить default workspace;
|
||||
2. добавить `workspace_id` в `operations` и `auth_profiles`;
|
||||
3. добавить `agents` и `published_agents`;
|
||||
4. внедрить `platform_api_keys`;
|
||||
5. добавить `invocation_logs` и `usage_rollups`;
|
||||
6. перевести MCP runtime на `published_agents`, а не на глобальный список operations.
|
||||
|
||||
+70
-306
@@ -2,14 +2,11 @@
|
||||
|
||||
## 1. Назначение документа
|
||||
|
||||
Этот документ собирает диаграммы, которые фиксируют проект до начала разработки:
|
||||
Этот документ собирает диаграммы целевой модели проекта:
|
||||
|
||||
- компонентную структуру;
|
||||
- связи между доменными сущностями;
|
||||
- хранение данных в БД;
|
||||
- основные runtime и admin-потоки.
|
||||
|
||||
Диаграммы даны в формате `Mermaid`, чтобы их можно было хранить прямо в репозитории и рендерить в Markdown-compatible tooling.
|
||||
- хранение данных в БД.
|
||||
|
||||
## 2. Компонентная диаграмма
|
||||
|
||||
@@ -29,6 +26,7 @@ flowchart LR
|
||||
GRPC[adapter-grpc]
|
||||
DB[(PostgreSQL)]
|
||||
STORE[(Artifact Storage)]
|
||||
OBS[(Usage and Logs)]
|
||||
|
||||
UI --> ADMIN
|
||||
MCP --> REG
|
||||
@@ -36,352 +34,118 @@ flowchart LR
|
||||
ADMIN --> REG
|
||||
ADMIN --> RUN
|
||||
ADMIN --> PROTO
|
||||
|
||||
REG --> DB
|
||||
REG --> CORE
|
||||
REG --> SCHEMA
|
||||
REG --> MAP
|
||||
|
||||
RUN --> CORE
|
||||
RUN --> SCHEMA
|
||||
RUN --> MAP
|
||||
RUN --> REST
|
||||
RUN --> GQL
|
||||
RUN --> GRPC
|
||||
|
||||
GRPC --> PROTO
|
||||
PROTO --> STORE
|
||||
ADMIN --> STORE
|
||||
REG --> OBS
|
||||
ADMIN --> OBS
|
||||
```
|
||||
|
||||
## 3. Диаграмма зависимостей crates
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
CORE[crank-core]
|
||||
SCHEMA[crank-schema]
|
||||
MAP[crank-mapping]
|
||||
PROTO[crank-proto]
|
||||
REG[crank-registry]
|
||||
RUN[crank-runtime]
|
||||
REST[crank-adapter-rest]
|
||||
GQL[crank-adapter-graphql]
|
||||
GRPC[crank-adapter-grpc]
|
||||
ADMIN[apps/admin-api]
|
||||
MCP[apps/mcp-server]
|
||||
|
||||
SCHEMA --> CORE
|
||||
MAP --> CORE
|
||||
PROTO --> CORE
|
||||
PROTO --> SCHEMA
|
||||
REG --> CORE
|
||||
REG --> SCHEMA
|
||||
REG --> MAP
|
||||
REST --> CORE
|
||||
GQL --> CORE
|
||||
GRPC --> CORE
|
||||
GRPC --> PROTO
|
||||
RUN --> CORE
|
||||
RUN --> SCHEMA
|
||||
RUN --> MAP
|
||||
RUN --> REST
|
||||
RUN --> GQL
|
||||
RUN --> GRPC
|
||||
ADMIN --> CORE
|
||||
ADMIN --> SCHEMA
|
||||
ADMIN --> MAP
|
||||
ADMIN --> PROTO
|
||||
ADMIN --> REG
|
||||
ADMIN --> RUN
|
||||
MCP --> CORE
|
||||
MCP --> REG
|
||||
MCP --> RUN
|
||||
```
|
||||
|
||||
## 4. Структурная диаграмма доменной модели
|
||||
## 3. Структурная диаграмма доменной модели
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class Workspace {
|
||||
+id
|
||||
+slug
|
||||
+display_name
|
||||
}
|
||||
|
||||
class Operation {
|
||||
+id
|
||||
+workspace_id
|
||||
+name
|
||||
+display_name
|
||||
+protocol
|
||||
+status
|
||||
+version
|
||||
+target
|
||||
+input_schema
|
||||
+output_schema
|
||||
+input_mapping
|
||||
+output_mapping
|
||||
+execution_config
|
||||
+tool_description
|
||||
+samples
|
||||
+generated_draft
|
||||
+config_export
|
||||
}
|
||||
|
||||
class RestTarget {
|
||||
+base_url
|
||||
+method
|
||||
+path_template
|
||||
+static_headers
|
||||
}
|
||||
|
||||
class GraphqlTarget {
|
||||
+endpoint
|
||||
+operation_type
|
||||
+operation_name
|
||||
+query_template
|
||||
+response_path
|
||||
}
|
||||
|
||||
class GrpcTarget {
|
||||
+server_addr
|
||||
+package
|
||||
+service
|
||||
+method
|
||||
+descriptor_ref
|
||||
+descriptor_set_b64
|
||||
}
|
||||
|
||||
class Schema {
|
||||
+type
|
||||
+description
|
||||
+fields
|
||||
}
|
||||
|
||||
class MappingSet {
|
||||
+rules[]
|
||||
}
|
||||
|
||||
class MappingRule {
|
||||
+source
|
||||
+target
|
||||
+required
|
||||
+default_value
|
||||
+transform
|
||||
+condition
|
||||
}
|
||||
|
||||
class ExecutionConfig {
|
||||
+timeout_ms
|
||||
+retry_policy
|
||||
+auth_profile_ref
|
||||
+headers
|
||||
+protocol_options
|
||||
}
|
||||
|
||||
class AuthProfile {
|
||||
class Agent {
|
||||
+id
|
||||
+name
|
||||
+kind
|
||||
+config
|
||||
}
|
||||
|
||||
class ToolDescription {
|
||||
+title
|
||||
+description
|
||||
+tags
|
||||
+examples
|
||||
}
|
||||
|
||||
class Samples {
|
||||
+input_json_sample_ref
|
||||
+output_json_sample_ref
|
||||
+proto_file_ref
|
||||
+descriptor_ref
|
||||
}
|
||||
|
||||
class GeneratedDraft {
|
||||
+workspace_id
|
||||
+slug
|
||||
+display_name
|
||||
+status
|
||||
+source_types
|
||||
+generated_at
|
||||
+warnings
|
||||
}
|
||||
|
||||
Operation --> RestTarget : target
|
||||
Operation --> GraphqlTarget : target
|
||||
Operation --> GrpcTarget : target
|
||||
Operation --> Schema : input_schema
|
||||
Operation --> Schema : output_schema
|
||||
Operation --> MappingSet : input_mapping
|
||||
Operation --> MappingSet : output_mapping
|
||||
Operation --> ExecutionConfig : execution_config
|
||||
Operation --> ToolDescription : tool_description
|
||||
Operation --> Samples : samples
|
||||
Operation --> GeneratedDraft : generated_draft
|
||||
ExecutionConfig --> AuthProfile : auth_profile_ref
|
||||
MappingSet --> MappingRule : contains
|
||||
class AgentBinding {
|
||||
+operation_id
|
||||
+operation_version
|
||||
+tool_name
|
||||
+enabled
|
||||
}
|
||||
|
||||
class PlatformApiKey {
|
||||
+id
|
||||
+workspace_id
|
||||
+name
|
||||
+prefix
|
||||
+scopes
|
||||
+status
|
||||
}
|
||||
|
||||
class InvocationLog {
|
||||
+workspace_id
|
||||
+agent_id
|
||||
+operation_id
|
||||
+status
|
||||
+duration_ms
|
||||
}
|
||||
|
||||
Workspace --> Operation : owns
|
||||
Workspace --> Agent : owns
|
||||
Workspace --> PlatformApiKey : owns
|
||||
Agent --> AgentBinding : contains
|
||||
AgentBinding --> Operation : references
|
||||
InvocationLog --> Workspace : belongs_to
|
||||
InvocationLog --> Agent : belongs_to
|
||||
InvocationLog --> Operation : belongs_to
|
||||
```
|
||||
|
||||
## 5. ER-диаграмма БД
|
||||
## 4. ER-диаграмма БД
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
WORKSPACES ||--o{ OPERATIONS : owns
|
||||
WORKSPACES ||--o{ AGENTS : owns
|
||||
WORKSPACES ||--o{ AUTH_PROFILES : owns
|
||||
WORKSPACES ||--o{ PLATFORM_API_KEYS : owns
|
||||
WORKSPACES ||--o{ INVOCATION_LOGS : owns
|
||||
OPERATIONS ||--o{ OPERATION_VERSIONS : has
|
||||
OPERATIONS ||--o| PUBLISHED_OPERATIONS : publishes
|
||||
OPERATIONS ||--o{ OPERATION_SAMPLES : owns
|
||||
OPERATIONS ||--o{ DESCRIPTORS : may_use
|
||||
OPERATIONS ||--o{ YAML_IMPORT_JOBS : may_create
|
||||
AUTH_PROFILES ||--o{ OPERATION_VERSIONS : referenced_by
|
||||
AGENTS ||--o{ AGENT_VERSIONS : has
|
||||
AGENTS ||--o| PUBLISHED_AGENTS : publishes
|
||||
AGENT_VERSIONS ||--o{ AGENT_OPERATION_BINDINGS : contains
|
||||
OPERATIONS ||--o{ AGENT_OPERATION_BINDINGS : exposed_by
|
||||
|
||||
WORKSPACES {
|
||||
text id PK
|
||||
text slug
|
||||
text display_name
|
||||
}
|
||||
OPERATIONS {
|
||||
text id PK
|
||||
text workspace_id FK
|
||||
text name
|
||||
text display_name
|
||||
text protocol
|
||||
text status
|
||||
int current_draft_version
|
||||
int latest_published_version
|
||||
timestamptz created_at
|
||||
timestamptz updated_at
|
||||
timestamptz published_at
|
||||
}
|
||||
|
||||
OPERATION_VERSIONS {
|
||||
text operation_id FK
|
||||
int version
|
||||
AGENTS {
|
||||
text id PK
|
||||
text workspace_id FK
|
||||
text slug
|
||||
text display_name
|
||||
text status
|
||||
jsonb target_json
|
||||
jsonb input_schema_json
|
||||
jsonb output_schema_json
|
||||
jsonb input_mapping_json
|
||||
jsonb output_mapping_json
|
||||
jsonb execution_config_json
|
||||
jsonb tool_description_json
|
||||
jsonb samples_json
|
||||
jsonb generated_draft_json
|
||||
jsonb config_export_json
|
||||
timestamptz created_at
|
||||
}
|
||||
|
||||
PUBLISHED_OPERATIONS {
|
||||
text operation_id PK
|
||||
int version
|
||||
timestamptz published_at
|
||||
text published_by
|
||||
}
|
||||
|
||||
OPERATION_SAMPLES {
|
||||
text id PK
|
||||
text operation_id FK
|
||||
int version
|
||||
text sample_kind
|
||||
text storage_ref
|
||||
text content_type
|
||||
text file_name
|
||||
timestamptz created_at
|
||||
}
|
||||
|
||||
DESCRIPTORS {
|
||||
text id PK
|
||||
text operation_id FK
|
||||
int version
|
||||
text descriptor_kind
|
||||
text storage_ref
|
||||
jsonb package_index_json
|
||||
timestamptz created_at
|
||||
}
|
||||
|
||||
YAML_IMPORT_JOBS {
|
||||
text id PK
|
||||
text source_sample_id
|
||||
text status
|
||||
text format_version
|
||||
text mode
|
||||
text result_operation_id
|
||||
int result_version
|
||||
text error_text
|
||||
timestamptz created_at
|
||||
timestamptz finished_at
|
||||
}
|
||||
|
||||
AUTH_PROFILES {
|
||||
text id PK
|
||||
text name
|
||||
text kind
|
||||
jsonb config_json
|
||||
timestamptz created_at
|
||||
timestamptz updated_at
|
||||
}
|
||||
```
|
||||
|
||||
## 6. Sequence: создание и публикация operation
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant UI
|
||||
participant API as admin-api
|
||||
participant REG as registry
|
||||
participant RUN as runtime
|
||||
participant MCP as mcp-server
|
||||
|
||||
UI->>API: POST /operations
|
||||
API->>REG: create operation v1
|
||||
REG-->>API: created
|
||||
API-->>UI: operation_id, version
|
||||
|
||||
UI->>API: upload samples / descriptors
|
||||
API-->>UI: artifact refs
|
||||
|
||||
UI->>API: POST /drafts/generate
|
||||
API->>REG: save generated draft metadata
|
||||
API-->>UI: generated draft
|
||||
|
||||
UI->>API: POST /test-runs
|
||||
API->>RUN: execute draft version
|
||||
RUN-->>API: request_preview + response_preview
|
||||
API-->>UI: test result
|
||||
|
||||
UI->>API: POST /publish
|
||||
API->>REG: mark version as published
|
||||
REG-->>API: published
|
||||
API-->>MCP: reload signal
|
||||
API-->>UI: published_version
|
||||
```
|
||||
|
||||
## 7. Sequence: MCP tool execution
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as MCP Client
|
||||
participant MCP as mcp-server
|
||||
participant REG as registry/cache
|
||||
participant RUN as runtime
|
||||
participant ADP as protocol adapter
|
||||
|
||||
Client->>MCP: call tool(name, input)
|
||||
MCP->>REG: resolve published runtime view
|
||||
REG-->>MCP: runtime operation
|
||||
MCP->>RUN: execute(operation, input)
|
||||
RUN->>RUN: validate input schema
|
||||
RUN->>RUN: apply input mapping
|
||||
RUN->>ADP: execute prepared request
|
||||
ADP-->>RUN: normalized response
|
||||
RUN->>RUN: apply output mapping
|
||||
RUN-->>MCP: output
|
||||
MCP-->>Client: tool result
|
||||
```
|
||||
|
||||
## 8. Sequence: YAML import
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant UI
|
||||
participant API as admin-api
|
||||
participant REG as registry
|
||||
|
||||
UI->>API: POST /operations/import (YAML)
|
||||
API->>API: parse YAML
|
||||
API->>API: validate schema, target, mapping
|
||||
API->>REG: create or upsert new version
|
||||
REG-->>API: operation_id, version
|
||||
API-->>UI: import result
|
||||
```
|
||||
|
||||
## 9. Что важно помнить
|
||||
|
||||
- Диаграммы фиксируют целевую архитектуру, а не точную реализацию каждого файла.
|
||||
- Если меняется модель данных или поток исполнения, сначала нужно обновлять документы, потом код.
|
||||
- Для старта разработки этого набора достаточно: компоненты, сущности, БД и ключевые sequence flows уже описаны.
|
||||
|
||||
+51
-355
@@ -2,425 +2,121 @@
|
||||
|
||||
## 1. Назначение документа
|
||||
|
||||
Этот документ фиксирует порядок реализации модулей и фич. Он нужен затем, чтобы разработка шла последовательно, а не параллельно во все стороны сразу.
|
||||
Этот документ фиксирует порядок перехода от текущего состояния проекта к целевой модели, заданной `test-ui`.
|
||||
|
||||
Принцип:
|
||||
|
||||
- сначала фундамент;
|
||||
- потом минимальный end-to-end сценарий;
|
||||
- потом расширение протоколов;
|
||||
- сначала перепроектирование `as is -> to be`;
|
||||
- потом foundation под workspace/agent model;
|
||||
- потом возврат к end-to-end UI сценариям;
|
||||
- потом observability и access layer;
|
||||
- потом polish и demo readiness.
|
||||
|
||||
## 2. Этап 0. Scaffold проекта
|
||||
## 2. Этап 1. Перепроектирование `As Is -> To Be`
|
||||
|
||||
Цель:
|
||||
|
||||
- создать `cargo workspace`;
|
||||
- создать приложения и crates;
|
||||
- подключить базовый CI/test workflow;
|
||||
- зафиксировать структуру каталогов.
|
||||
|
||||
Состав:
|
||||
|
||||
- `apps/admin-api`
|
||||
- `apps/mcp-server`
|
||||
- `apps/ui`
|
||||
- `crates/crank-core`
|
||||
- `crates/crank-schema`
|
||||
- `crates/crank-mapping`
|
||||
- `crates/crank-proto`
|
||||
- `crates/crank-registry`
|
||||
- `crates/crank-runtime`
|
||||
- `crates/crank-adapter-rest`
|
||||
- `crates/crank-adapter-graphql`
|
||||
- `crates/crank-adapter-grpc`
|
||||
|
||||
Результат:
|
||||
|
||||
- проект собирается;
|
||||
- тестовый pipeline запускается;
|
||||
- есть пустые crate boundaries.
|
||||
- зафиксировать новую доменную модель и page-driven backend contract.
|
||||
|
||||
DoD:
|
||||
|
||||
- создан `cargo workspace`;
|
||||
- все crates и apps объявлены в workspace;
|
||||
- проект собирается без бизнес-логики;
|
||||
- базовые test targets запускаются;
|
||||
- сделан атомарный commit со scaffold.
|
||||
- зафиксирован `as is -> to be` план;
|
||||
- page-by-page gap analysis покрывает все целевые экраны;
|
||||
- разобраны все архитектурные конфликты UI vs current backend;
|
||||
- документы `architecture`, `data-model`, `database-schema`, `admin-api`, `mcp-interface` синхронизированы.
|
||||
|
||||
## 3. Этап 1. Базовая доменная модель
|
||||
## 3. Этап 2. Workspace foundation
|
||||
|
||||
Цель:
|
||||
|
||||
- реализовать типы из `data-model`.
|
||||
|
||||
Фичи:
|
||||
|
||||
- `Operation`
|
||||
- `Target`
|
||||
- `Schema`
|
||||
- `MappingSet`
|
||||
- `ExecutionConfig`
|
||||
- `ToolDescription`
|
||||
- `AuthProfile`
|
||||
|
||||
Параллельно:
|
||||
|
||||
- unit tests на доменные типы;
|
||||
- базовая сериализация `JSON`/`YAML`.
|
||||
|
||||
Результат:
|
||||
|
||||
- модель данных существует как код;
|
||||
- нет инфраструктурных зависимостей внутри домена.
|
||||
- перевести хранение и API на workspace-scoped модель.
|
||||
|
||||
DoD:
|
||||
|
||||
- типы из `data-model` реализованы;
|
||||
- базовая сериализация `JSON` и `YAML` проходит тесты;
|
||||
- доменные `impl` не содержат инфраструктурной логики;
|
||||
- unit tests на ключевые типы проходят;
|
||||
- изменения зафиксированы через один или несколько `RGR + commit`.
|
||||
- операции и auth profiles принадлежат workspace;
|
||||
- registry умеет фильтровать данные по workspace;
|
||||
- есть default workspace migration path.
|
||||
|
||||
## 4. Этап 2. Schema engine
|
||||
## 4. Этап 3. Agent publishing foundation
|
||||
|
||||
Цель:
|
||||
|
||||
- реализовать `crank-schema`.
|
||||
|
||||
Фичи:
|
||||
|
||||
- model полей и типов;
|
||||
- schema validation;
|
||||
- field traversal;
|
||||
- нормализация JSON samples;
|
||||
- protobuf -> schema bridge contracts.
|
||||
|
||||
Результат:
|
||||
|
||||
- можно описывать и валидировать вход/выход.
|
||||
- ввести `Agent` и agent-scoped MCP publishing.
|
||||
|
||||
DoD:
|
||||
|
||||
- реализована схема полей и типов;
|
||||
- работает schema validation;
|
||||
- JSON sample normalization покрыт тестами;
|
||||
- контракты protobuf -> schema зафиксированы;
|
||||
- нет смешивания schema logic с adapter logic.
|
||||
- можно создать agent и привязать к нему published operations;
|
||||
- `mcp-server` выдает tools в контексте конкретного agent;
|
||||
- один agent видит только свой curated toolset.
|
||||
|
||||
## 5. Этап 3. Mapping engine
|
||||
## 5. Этап 4. Operations and wizard integration
|
||||
|
||||
Цель:
|
||||
|
||||
- реализовать `crank-mapping`.
|
||||
|
||||
Фичи:
|
||||
|
||||
- `JSONPath` parsing и validation;
|
||||
- input mapping;
|
||||
- output mapping;
|
||||
- transforms;
|
||||
- generation draft mapping из samples.
|
||||
|
||||
Результат:
|
||||
|
||||
- можно преобразовывать MCP input в request model и response в output model.
|
||||
- посадить operations catalog и wizard на реальные backend contracts.
|
||||
|
||||
DoD:
|
||||
|
||||
- `JSONPath` parsing и validation работают;
|
||||
- input/output mapping проходят unit tests;
|
||||
- generation draft mapping покрыта фикстурами;
|
||||
- transforms ограничены и задокументированы;
|
||||
- mapping engine не знает о конкретных protocol adapters.
|
||||
- каталог операций и wizard работают без `localStorage` overrides;
|
||||
- operation edit/delete/publish/test выполняются через backend;
|
||||
- все протоколы работают в рамках одного UI flow.
|
||||
|
||||
## 6. Этап 4. Registry и БД
|
||||
## 6. Этап 5. Agents UI and backend
|
||||
|
||||
Цель:
|
||||
|
||||
- реализовать `crank-registry` и миграции.
|
||||
|
||||
Фичи:
|
||||
|
||||
- таблицы из `database-schema`;
|
||||
- version snapshots;
|
||||
- published operations;
|
||||
- auth profiles;
|
||||
- sample metadata;
|
||||
- descriptor metadata;
|
||||
- YAML import job log.
|
||||
|
||||
Результат:
|
||||
|
||||
- конфигурации можно хранить и версионировать.
|
||||
- реализовать agent-centric слой.
|
||||
|
||||
DoD:
|
||||
|
||||
- миграции создают таблицы из `database-schema`;
|
||||
- version snapshots работают корректно;
|
||||
- publish linkage реализован;
|
||||
- auth profiles и artifact metadata сохраняются;
|
||||
- integration tests на registry проходят на реальной БД.
|
||||
- agent CRUD работает;
|
||||
- binding operations к agent работает;
|
||||
- published agent появляется в MCP runtime.
|
||||
|
||||
## 7. Этап 5. REST vertical slice
|
||||
## 7. Этап 6. Platform access
|
||||
|
||||
Цель:
|
||||
|
||||
- получить первый рабочий end-to-end сценарий.
|
||||
|
||||
Фичи:
|
||||
|
||||
- `crank-adapter-rest`
|
||||
- `crank-runtime` для REST
|
||||
- REST test run
|
||||
- создание REST operation
|
||||
- publish REST operation
|
||||
- вызов published REST tool из MCP слоя
|
||||
|
||||
Результат:
|
||||
|
||||
- MVP работает хотя бы для REST.
|
||||
- реализовать workspace access и platform API keys.
|
||||
|
||||
DoD:
|
||||
|
||||
- REST operation можно создать, протестировать и опубликовать;
|
||||
- runtime исполняет REST operation end-to-end;
|
||||
- published REST tool вызывается через MCP слой;
|
||||
- negative tests на mapping и external errors существуют;
|
||||
- есть демонстрационный REST сценарий.
|
||||
- UI screens `API Keys`, `Settings`, `Workspace` имеют backend-контракт;
|
||||
- platform API keys не смешиваются с upstream auth profiles;
|
||||
- tenant boundary выражен в access layer.
|
||||
|
||||
## 8. Этап 6. Admin API v1
|
||||
## 8. Этап 7. Observability
|
||||
|
||||
Цель:
|
||||
|
||||
- дать UI полный backend-контракт для базового сценария.
|
||||
|
||||
Фичи:
|
||||
|
||||
- CRUD operations;
|
||||
- create version;
|
||||
- publish;
|
||||
- upload input/output JSON samples;
|
||||
- generate draft;
|
||||
- test run;
|
||||
- auth profiles CRUD;
|
||||
- YAML import/export.
|
||||
|
||||
Результат:
|
||||
|
||||
- UI может полностью управлять REST operation без ручных правок кода.
|
||||
- реализовать логи и usage.
|
||||
|
||||
DoD:
|
||||
|
||||
- доступны CRUD, versioning, publish, samples, draft generation, test runs;
|
||||
- доступны auth profiles и YAML import/export;
|
||||
- API контракты соответствуют документации;
|
||||
- integration tests на ключевые endpoints проходят;
|
||||
- нет скрытой бизнес-логики в handlers.
|
||||
- `Logs` page и `Usage` page работают на реальных данных;
|
||||
- есть продуктовые endpoints, а не только application logs;
|
||||
- rollups и detail views согласованы с UI.
|
||||
|
||||
## 9. Этап 7. UI v1
|
||||
## 9. Этап 8. Alpine UI integration
|
||||
|
||||
Цель:
|
||||
|
||||
- собрать рабочую административную консоль.
|
||||
|
||||
Фичи:
|
||||
|
||||
- список операций;
|
||||
- мастер создания операции;
|
||||
- sample upload;
|
||||
- schema viewer;
|
||||
- mapping editor;
|
||||
- test run screen;
|
||||
- publish flow;
|
||||
- YAML import/export screen.
|
||||
|
||||
Результат:
|
||||
|
||||
- есть демонстрируемый пользовательский интерфейс.
|
||||
- перенести `test-ui` в `apps/ui` и подключить его к реальному backend.
|
||||
|
||||
DoD:
|
||||
|
||||
- UI покрывает основной сценарий от создания operation до publish;
|
||||
- sample upload и mapping editor работают;
|
||||
- YAML import/export доступен из UI;
|
||||
- нет блокирующих заглушек на критическом пути демо;
|
||||
- основные пользовательские сценарии проверены вручную или integration tests.
|
||||
- `apps/ui` содержит целевой Alpine.js UI;
|
||||
- mock JSON больше не используется на критическом пути;
|
||||
- UI, backend и docs синхронизированы.
|
||||
|
||||
## 10. Этап 8. MCP server
|
||||
## 10. Этап 9. Hardening and demo readiness
|
||||
|
||||
Цель:
|
||||
|
||||
- публиковать published operations как MCP tools.
|
||||
|
||||
Фичи:
|
||||
|
||||
- `Streamable HTTP`;
|
||||
- list tools;
|
||||
- call tool;
|
||||
- reload published tools;
|
||||
- error mapping MCP layer.
|
||||
|
||||
Результат:
|
||||
|
||||
- REST operation доступна как полноценный MCP tool.
|
||||
- довести продукт до стабильного демо-сценария.
|
||||
|
||||
DoD:
|
||||
|
||||
- `Streamable HTTP` transport работает;
|
||||
- list tools и call tool реализованы;
|
||||
- reload published tools работает без перезапуска;
|
||||
- ошибки runtime корректно транслируются в MCP слой;
|
||||
- есть end-to-end test или demo flow вызова published REST tool.
|
||||
|
||||
## 11. Этап 9. GraphQL support
|
||||
|
||||
Цель:
|
||||
|
||||
- добавить второй протокол без разрушения архитектуры.
|
||||
|
||||
Фичи:
|
||||
|
||||
- `crank-adapter-graphql`
|
||||
- GraphQL target support;
|
||||
- variables mapping;
|
||||
- `response_path`;
|
||||
- GraphQL test runs;
|
||||
- publish и вызов через MCP.
|
||||
|
||||
Результат:
|
||||
|
||||
- второй end-to-end сценарий.
|
||||
|
||||
DoD:
|
||||
|
||||
- GraphQL operation можно создать, протестировать и опубликовать;
|
||||
- variables mapping и `response_path` работают;
|
||||
- GraphQL `errors` корректно обрабатываются;
|
||||
- published GraphQL tool вызывается через MCP слой;
|
||||
- есть demo fixture или integration scenario.
|
||||
|
||||
## 12. Этап 10. gRPC support
|
||||
|
||||
Цель:
|
||||
|
||||
- добавить unary gRPC.
|
||||
|
||||
Фичи:
|
||||
|
||||
- `crank-proto`
|
||||
- descriptor loading;
|
||||
- service/method discovery;
|
||||
- protobuf normalization;
|
||||
- `crank-adapter-grpc`
|
||||
- unary test runs;
|
||||
- publish и вызов через MCP.
|
||||
|
||||
Результат:
|
||||
|
||||
- третий end-to-end сценарий.
|
||||
|
||||
DoD:
|
||||
|
||||
- `.proto` или descriptor set можно загрузить;
|
||||
- unary method discovery работает;
|
||||
- JSON <-> protobuf conversion покрыта тестами;
|
||||
- gRPC operation можно протестировать и опубликовать;
|
||||
- published gRPC tool вызывается через MCP слой.
|
||||
|
||||
## 13. Этап 11. Harden и demo readiness
|
||||
|
||||
Цель:
|
||||
|
||||
- довести проект до стабильного demo state.
|
||||
|
||||
Фичи:
|
||||
|
||||
- логирование и tracing;
|
||||
- улучшение ошибок;
|
||||
- фикстуры и demo scenarios;
|
||||
- polish UI;
|
||||
- документация по запуску;
|
||||
- проверка YAML roundtrip;
|
||||
- проверка publish/reload flow.
|
||||
|
||||
DoD:
|
||||
|
||||
- демонстрационные сценарии воспроизводимы;
|
||||
- логирование и ошибки читаемы;
|
||||
- YAML roundtrip проверен;
|
||||
- publish/reload flow стабилен;
|
||||
- документация по запуску достаточна для повторения демо.
|
||||
|
||||
## 14. Этап 12. Deployment и CD
|
||||
|
||||
Цель:
|
||||
|
||||
- сделать повторяемый production-like запуск проекта.
|
||||
|
||||
Фичи:
|
||||
|
||||
- `Dockerfile` для приложений;
|
||||
- `docker-compose.yml`;
|
||||
- `.env.example`;
|
||||
- reverse proxy examples;
|
||||
- health endpoints;
|
||||
- CD workflow для `main`.
|
||||
|
||||
Результат:
|
||||
|
||||
- проект можно развернуть на Linux-хосте без ручной сборки бинарей и без ad-hoc shell-скриптов.
|
||||
|
||||
DoD:
|
||||
|
||||
- backend приложения собираются в контейнеры;
|
||||
- есть production-like compose конфигурация;
|
||||
- reverse proxy examples задокументированы;
|
||||
- CI и CD разделены;
|
||||
- deployment проверяется healthchecks.
|
||||
|
||||
## 15. Приоритеты по реализации
|
||||
|
||||
Если времени не хватает, сохраняется такой приоритет:
|
||||
|
||||
1. REST end-to-end
|
||||
2. Registry + versioning
|
||||
3. YAML import/export
|
||||
4. MCP server
|
||||
5. GraphQL
|
||||
6. gRPC
|
||||
|
||||
Причина:
|
||||
|
||||
- диплом должен показать работающую платформу;
|
||||
- лучше один полный вертикальный сценарий, чем три недоделанных адаптера.
|
||||
|
||||
## 16. Разбиение по фичам
|
||||
|
||||
Каждый этап желательно бить на маленькие фичи:
|
||||
|
||||
- `schema-field-model`
|
||||
- `schema-validator`
|
||||
- `jsonpath-parser`
|
||||
- `input-mapping-engine`
|
||||
- `output-mapping-engine`
|
||||
- `registry-create-version`
|
||||
- `registry-publish`
|
||||
- `rest-adapter-post-json`
|
||||
- `yaml-export-portable`
|
||||
- `mcp-list-tools`
|
||||
|
||||
Для каждой такой фичи локальный `DoD` должен быть записан до начала реализации хотя бы в рабочем описании задачи или ветки.
|
||||
|
||||
## 17. Практический итог
|
||||
|
||||
Правильная последовательность для проекта:
|
||||
|
||||
- сначала домен и фундамент;
|
||||
- потом registry;
|
||||
- потом один полный REST vertical slice;
|
||||
- потом admin-ui и MCP слой;
|
||||
- только после этого расширение на GraphQL и gRPC.
|
||||
|
||||
Такой порядок минимизирует архитектурный риск и дает ранний рабочий результат.
|
||||
- end-to-end demo воспроизводим;
|
||||
- deployment и healthchecks стабильно зелёные;
|
||||
- документация и продуктовый сценарий совпадают.
|
||||
|
||||
+47
-60
@@ -2,40 +2,34 @@
|
||||
|
||||
## 1. Назначение документа
|
||||
|
||||
Этот документ фиксирует, как именно платформа публикует operations в виде MCP tools и какой transport используется в MVP.
|
||||
|
||||
Главная цель - убрать неопределенность вокруг вопроса "каким именно будет MCP server" до начала реализации.
|
||||
Этот документ фиксирует, как именно платформа публикует agents и operations в виде MCP tools и какой transport используется в целевой модели.
|
||||
|
||||
## 2. Архитектурное решение
|
||||
|
||||
Для MVP `mcp-server` должен публиковать tools через network-oriented MCP transport.
|
||||
`mcp-server` публикует tools через network-oriented MCP transport.
|
||||
|
||||
Рекомендуемое решение:
|
||||
Решение:
|
||||
|
||||
- основной transport: `Streamable HTTP`;
|
||||
- отдельный `mcp-server` как сервис;
|
||||
- `stdio` не является обязательной частью MVP.
|
||||
|
||||
Причина:
|
||||
|
||||
- проект задуман как `Crank`, а не как локальный single-process adapter;
|
||||
- нужен удаленный доступ к опубликованным tools;
|
||||
- published tools должны обновляться без пересборки и без локального обертывания каждого клиента.
|
||||
- `stdio` не является обязательной частью текущего scope.
|
||||
|
||||
## 3. Модель публикации tools
|
||||
|
||||
Каждая published operation превращается в один MCP tool.
|
||||
Каждая published operation превращается в один MCP tool внутри конкретного published agent.
|
||||
|
||||
Соответствие:
|
||||
|
||||
- одна published version;
|
||||
- один tool name;
|
||||
- один published agent;
|
||||
- набор `AgentOperationBinding`;
|
||||
- один tool name на binding;
|
||||
- одна input schema;
|
||||
- один результат.
|
||||
|
||||
Публикация tool основана на:
|
||||
|
||||
- `operation.name`
|
||||
- `agent.slug`
|
||||
- `operation.name` или binding-level `tool_name`
|
||||
- `tool_description`
|
||||
- `input_schema`
|
||||
- `published runtime view`
|
||||
@@ -44,7 +38,7 @@
|
||||
|
||||
`mcp-server` должен:
|
||||
|
||||
- загрузить published operations из registry;
|
||||
- загрузить published agents и их bindings из registry;
|
||||
- преобразовать их в MCP tool definitions;
|
||||
- принимать вызовы tools от MCP clients;
|
||||
- валидировать вход;
|
||||
@@ -62,12 +56,12 @@
|
||||
- заниматься protobuf discovery;
|
||||
- содержать бизнес-логику admin UI.
|
||||
|
||||
## 6. Published runtime view
|
||||
## 6. Runtime view
|
||||
|
||||
`mcp-server` должен работать не с полной admin-конфигурацией, а с runtime-ready view.
|
||||
|
||||
В published runtime view остаются:
|
||||
В runtime view остаются:
|
||||
|
||||
- `workspace_id`
|
||||
- `agent_id`
|
||||
- `operation_id`
|
||||
- `protocol`
|
||||
- `target`
|
||||
@@ -78,29 +72,30 @@
|
||||
- `execution_config`
|
||||
- `tool_description`
|
||||
|
||||
В published runtime view не должны попадать:
|
||||
В runtime view не попадают:
|
||||
|
||||
- raw uploaded samples;
|
||||
- generated draft metadata;
|
||||
- YAML import metadata;
|
||||
- UI-specific helper fields.
|
||||
|
||||
## 7. Transport для MVP
|
||||
## 7. MCP endpoint model
|
||||
|
||||
### Поддерживается
|
||||
Канонический endpoint:
|
||||
|
||||
- `Streamable HTTP`
|
||||
```text
|
||||
/mcp/v1/{workspace_slug}/{agent_slug}
|
||||
```
|
||||
|
||||
### Не обязательно в MVP
|
||||
Этот endpoint определяет:
|
||||
|
||||
- `stdio`
|
||||
- дополнительные transport adapters
|
||||
|
||||
Если позже понадобится локальная интеграция, `stdio` можно добавить как отдельный transport layer поверх того же runtime.
|
||||
- tenant boundary;
|
||||
- конкретный curated toolset;
|
||||
- набор usage и log labels.
|
||||
|
||||
## 8. MCP lifecycle
|
||||
|
||||
MVP-контракт `mcp-server` строится вокруг JSON-RPC методов MCP:
|
||||
Поддерживаемые JSON-RPC методы:
|
||||
|
||||
- `initialize`
|
||||
- `notifications/initialized`
|
||||
@@ -108,37 +103,32 @@ MVP-контракт `mcp-server` строится вокруг JSON-RPC мет
|
||||
- `tools/list`
|
||||
- `tools/call`
|
||||
|
||||
Сессия создается на `initialize` и идентифицируется через `MCP-Session-Id`.
|
||||
Согласованная версия протокола возвращается и читается через `MCP-Protocol-Version`.
|
||||
|
||||
Пока сессии хранятся in-memory внутри `mcp-server`, чего достаточно для MVP и demo-сценариев.
|
||||
|
||||
### Tool listing
|
||||
|
||||
После `initialize` и `notifications/initialized`:
|
||||
|
||||
1. клиент вызывает `tools/list`;
|
||||
2. `mcp-server` перечитывает published operations по refresh policy;
|
||||
3. строит или обновляет in-memory catalog tools;
|
||||
4. отдает список tools через MCP JSON-RPC result.
|
||||
2. `mcp-server` извлекает `workspace_slug` и `agent_slug` из path;
|
||||
3. перечитывает published agent по refresh policy;
|
||||
4. строит или обновляет in-memory catalog tools только для этого agent;
|
||||
5. отдает список tools через MCP JSON-RPC result.
|
||||
|
||||
### Tool call
|
||||
|
||||
1. MCP client вызывает tool.
|
||||
2. `mcp-server` находит published runtime view.
|
||||
3. Валидирует input относительно schema.
|
||||
4. Делегирует вызов в `crank-runtime`.
|
||||
5. Возвращает результат.
|
||||
1. клиент вызывает tool;
|
||||
2. `mcp-server` определяет `workspace` и `agent`;
|
||||
3. находит binding нужной operation внутри published agent;
|
||||
4. валидирует input относительно schema;
|
||||
5. делегирует вызов в `crank-runtime`;
|
||||
6. возвращает результат.
|
||||
|
||||
## 9. Обновление tools
|
||||
|
||||
После публикации новой версии:
|
||||
После публикации новой operation version или agent version:
|
||||
|
||||
1. `admin-api` фиксирует published version в registry.
|
||||
2. `registry` обновляет published_operations.
|
||||
3. `mcp-server` не требует restart и не опирается на ручной reload signal.
|
||||
4. `mcp-server` выполняет controlled refresh опубликованного каталога по interval-based policy.
|
||||
5. Новый tool contract становится доступен MCP clients.
|
||||
1. `admin-api` фиксирует published version в registry;
|
||||
2. `registry` обновляет published operations или published agents;
|
||||
3. `mcp-server` не требует restart;
|
||||
4. выполняется controlled refresh опубликованного каталога;
|
||||
5. новый tool contract становится доступен MCP clients.
|
||||
|
||||
## 10. Именование tools
|
||||
|
||||
@@ -150,9 +140,9 @@ MVP-контракт `mcp-server` строится вокруг JSON-RPC мет
|
||||
|
||||
Требования:
|
||||
|
||||
- имя уникально в пределах платформы;
|
||||
- имя не зависит от внутреннего numeric version;
|
||||
- rename operation должен считаться отдельным осознанным изменением.
|
||||
- имя уникально в пределах одного agent;
|
||||
- имя не зависит от numeric version;
|
||||
- один и тот же operation может публиковаться под разными именами в разных agents.
|
||||
|
||||
## 11. Ошибки MCP слоя
|
||||
|
||||
@@ -164,14 +154,11 @@ MVP-контракт `mcp-server` строится вокруг JSON-RPC мет
|
||||
- external service error;
|
||||
- internal runtime error.
|
||||
|
||||
`mcp-server` не должен терять стадию ошибки при трансляции ответа клиенту.
|
||||
|
||||
## 12. Практический итог
|
||||
|
||||
Для MVP достаточно следующей фиксации:
|
||||
|
||||
- `mcp-server` - отдельный сервис;
|
||||
- transport - `Streamable HTTP`;
|
||||
- одна published operation = один MCP tool;
|
||||
- endpoint определяется парой `workspace + agent`;
|
||||
- одна published operation = один MCP tool внутри agent;
|
||||
- reload published tools без пересборки сервиса;
|
||||
- никакой draft-логики или admin CRUD в MCP слое.
|
||||
|
||||
+57
-622
@@ -2,44 +2,22 @@
|
||||
|
||||
## 1. Цель документа
|
||||
|
||||
Этот документ фиксирует детальную структуру проекта до начала активной разработки. Его задача - заранее ограничить ответственность каждого компонента, избежать разрастания `crank-core`, не допустить появления "универсальных" структур на все случаи жизни и сохранить понятные границы между доменной логикой, runtime, адаптерами, API и UI.
|
||||
Этот документ фиксирует детальную структуру проекта под целевую модель `workspace -> agent -> operations`.
|
||||
|
||||
Основной принцип: каждый crate отвечает за один слой системы. Внутри crate модули должны быть маленькими, тематическими и с минимальным количеством публичных сущностей.
|
||||
Основной принцип: каждый crate отвечает за один слой системы. Внутри crate модули маленькие, тематические и с минимальным количеством публичных сущностей.
|
||||
|
||||
## 2. Общие архитектурные правила
|
||||
|
||||
### 2.1. Что считается правильной декомпозицией
|
||||
|
||||
- `core` содержит только базовую доменную модель, идентификаторы, типы ошибок и общие контракты.
|
||||
- `registry` отвечает только за хранение и загрузку конфигурации операций.
|
||||
- `registry` отвечает только за хранение и загрузку workspace-scoped конфигурации.
|
||||
- `runtime` исполняет операции, но не знает о способе их хранения.
|
||||
- адаптеры знают только свой протокол и общий контракт runtime.
|
||||
- `admin-api` оркестрирует use case для UI, но не содержит протокольной логики.
|
||||
- `mcp-server` публикует tools и вызывает runtime, но не содержит бизнес-логики конфигурирования.
|
||||
- `ui` не знает внутреннюю реализацию runtime и работает только через HTTP API.
|
||||
|
||||
### 2.2. Что запрещено
|
||||
|
||||
- помещать SQL, HTTP-клиенты или gRPC-клиенты в `crank-core`;
|
||||
- хранить в `core` "общие утилиты", не относящиеся к доменной модели;
|
||||
- делать `runtime`, который напрямую читает БД;
|
||||
- писать mapping-логику внутри REST, GraphQL или gRPC адаптеров;
|
||||
- дублировать доменные типы в `admin-api`, `mcp-server` и адаптерах;
|
||||
- создавать большие структуры вида `AppState`, в которые складывается все подряд;
|
||||
- создавать большие enum или config-объекты, содержащие поля всех протоколов одновременно без выделенных вложенных типов.
|
||||
|
||||
### 2.3. Предпочтительный стиль
|
||||
|
||||
- узкие интерфейсы;
|
||||
- маленькие DTO;
|
||||
- отдельные типы для draft, published и runtime-view сущностей;
|
||||
- отдельные модули для чтения, записи, валидации и исполнения;
|
||||
- композиция из небольших сервисов вместо одного глобального сервиса.
|
||||
- `mcp-server` публикует agent-scoped tools и вызывает runtime.
|
||||
- `ui` работает только через HTTP API.
|
||||
|
||||
## 3. Workspace-структура
|
||||
|
||||
Рекомендуемая структура:
|
||||
|
||||
```text
|
||||
crank/
|
||||
apps/
|
||||
@@ -58,7 +36,11 @@ crank/
|
||||
crank-proto/
|
||||
```
|
||||
|
||||
Дополнительные crates `crank-mapping`, `crank-schema` и `crank-proto` нужны затем, чтобы не перегружать `crank-core`.
|
||||
Поверх существующих crates должны появиться новые логические поддомены:
|
||||
|
||||
- workspace/access domain;
|
||||
- agent publishing domain;
|
||||
- observability domain.
|
||||
|
||||
## 4. Детальная декомпозиция по crate
|
||||
|
||||
@@ -68,58 +50,19 @@ crank/
|
||||
|
||||
- базовые доменные типы;
|
||||
- идентификаторы;
|
||||
- метаданные операций;
|
||||
- общие контракты и ошибки верхнего уровня.
|
||||
- метаданные workspace, operation и agent;
|
||||
- общие контракты и ошибки.
|
||||
|
||||
Что должно лежать в crate:
|
||||
Внутренние модули:
|
||||
|
||||
```text
|
||||
crank-core/
|
||||
src/
|
||||
lib.rs
|
||||
ids.rs
|
||||
protocol.rs
|
||||
operation/
|
||||
mod.rs
|
||||
model.rs
|
||||
status.rs
|
||||
target.rs
|
||||
metadata.rs
|
||||
auth/
|
||||
mod.rs
|
||||
profile.rs
|
||||
secret_ref.rs
|
||||
errors/
|
||||
mod.rs
|
||||
domain.rs
|
||||
validation.rs
|
||||
runtime.rs
|
||||
```
|
||||
|
||||
Описание модулей:
|
||||
|
||||
- `ids.rs` - типы `OperationId`, `DescriptorId`, `ToolId` и другие идентификаторы.
|
||||
- `protocol.rs` - enum протоколов и общие protocol capability flags.
|
||||
- `operation/model.rs` - основная доменная модель операции без технических деталей хранения.
|
||||
- `operation/status.rs` - типы состояний операции.
|
||||
- `operation/target.rs` - базовые protocol-specific target structs.
|
||||
- `operation/metadata.rs` - описание tool, display name, version, tags.
|
||||
- `auth/profile.rs` - типы auth-профилей без привязки к конкретному клиенту.
|
||||
- `auth/secret_ref.rs` - ссылки на секреты, а не сами секреты.
|
||||
- `errors/*` - типизированные ошибки доменного слоя.
|
||||
|
||||
Что не должно лежать в crate:
|
||||
|
||||
- JSON Schema реализация;
|
||||
- mapping engine;
|
||||
- SQL-модели;
|
||||
- HTTP DTO;
|
||||
- protobuf parsing;
|
||||
- `reqwest`, `sqlx`, `tonic`, `axum`.
|
||||
|
||||
Причина:
|
||||
|
||||
`crank-core` должен быть максимально стабильным и независимым. Если положить туда все подряд, он станет точкой связности всей системы.
|
||||
- `ids`
|
||||
- `protocol`
|
||||
- `workspace`
|
||||
- `operation`
|
||||
- `agent`
|
||||
- `auth`
|
||||
- `observability`
|
||||
- `errors`
|
||||
|
||||
### 4.2. `crank-schema`
|
||||
|
||||
@@ -127,107 +70,16 @@ crank-core/
|
||||
|
||||
- внутренняя модель схем;
|
||||
- нормализация входа и выхода;
|
||||
- представление полей для UI и runtime;
|
||||
- преобразование схем из разных источников в единый вид.
|
||||
|
||||
Структура:
|
||||
|
||||
```text
|
||||
crank-schema/
|
||||
src/
|
||||
lib.rs
|
||||
schema/
|
||||
mod.rs
|
||||
model.rs
|
||||
field.rs
|
||||
scalar.rs
|
||||
object.rs
|
||||
collection.rs
|
||||
oneof.rs
|
||||
enums.rs
|
||||
normalize/
|
||||
mod.rs
|
||||
json.rs
|
||||
graphql.rs
|
||||
protobuf.rs
|
||||
validate/
|
||||
mod.rs
|
||||
input.rs
|
||||
output.rs
|
||||
```
|
||||
|
||||
Описание:
|
||||
|
||||
- `schema/model.rs` - корневая структура схемы.
|
||||
- `field.rs` - описание поля, nullable, required, description.
|
||||
- `scalar.rs` - базовые scalar types.
|
||||
- `object.rs` - вложенные объекты.
|
||||
- `collection.rs` - массивы и map-подобные структуры.
|
||||
- `oneof.rs` - представление protobuf `oneof`.
|
||||
- `enums.rs` - enum-значения и метаданные.
|
||||
- `normalize/*` - преобразование GraphQL и protobuf моделей в единую схему.
|
||||
- `normalize/json.rs` - нормализация загруженных JSON-примеров во внутреннюю schema model.
|
||||
- `validate/*` - проверка JSON относительно внутренней схемы.
|
||||
|
||||
Почему отдельный crate:
|
||||
|
||||
Схемы будут использоваться почти везде, но это не повод тащить их в `core`. Иначе `core` станет тяжелым и начнет менять версию при каждом изменении схемной логики.
|
||||
- представление полей для UI и runtime.
|
||||
|
||||
### 4.3. `crank-mapping`
|
||||
|
||||
Назначение:
|
||||
|
||||
- описание mapping DSL;
|
||||
- компиляция mappings в runtime-представление;
|
||||
- применение mappings к входу и выходу;
|
||||
- автогенерация чернового mapping по загруженным примерам;
|
||||
- трассировка ошибок маппинга.
|
||||
|
||||
Структура:
|
||||
|
||||
```text
|
||||
crank-mapping/
|
||||
src/
|
||||
lib.rs
|
||||
model/
|
||||
mod.rs
|
||||
mapping.rs
|
||||
source.rs
|
||||
target.rs
|
||||
transform.rs
|
||||
parser/
|
||||
mod.rs
|
||||
jsonpath.rs
|
||||
compile/
|
||||
mod.rs
|
||||
plan.rs
|
||||
infer/
|
||||
mod.rs
|
||||
from_samples.rs
|
||||
from_schema.rs
|
||||
execute/
|
||||
mod.rs
|
||||
input.rs
|
||||
output.rs
|
||||
errors.rs
|
||||
```
|
||||
|
||||
Описание:
|
||||
|
||||
- `model/mapping.rs` - описание одного правила mapping.
|
||||
- `model/source.rs` - откуда берем данные: `mcp`, `response`, `constant`.
|
||||
- `model/target.rs` - куда кладем данные: `request.path`, `request.query`, `request.body`, `output`.
|
||||
- `model/transform.rs` - ограниченный набор допустимых преобразований.
|
||||
- `parser/jsonpath.rs` - единый parser и validator для `JSONPath` выражений.
|
||||
- `compile/plan.rs` - предварительно скомпилированный план маппинга.
|
||||
- `infer/from_samples.rs` - генерация чернового mapping по загруженным примерам JSON.
|
||||
- `infer/from_schema.rs` - генерация чернового mapping по нормализованной схеме.
|
||||
- `execute/input.rs` - применение mappings к запросу.
|
||||
- `execute/output.rs` - применение mappings к ответу.
|
||||
|
||||
Антипаттерн, которого нужно избежать:
|
||||
|
||||
не помещать mapping-правила в строковые поля, которые потом интерпретируются каждым адаптером по-своему. Mapping должен быть единым движком.
|
||||
- mapping DSL;
|
||||
- `JSONPath` parsing;
|
||||
- input/output mapping;
|
||||
- draft inference из samples.
|
||||
|
||||
### 4.4. `crank-proto`
|
||||
|
||||
@@ -237,472 +89,55 @@ crank-mapping/
|
||||
- извлечение services, methods и message schemas;
|
||||
- преобразование protobuf metadata во внутренние типы.
|
||||
|
||||
Структура:
|
||||
|
||||
```text
|
||||
crank-proto/
|
||||
src/
|
||||
lib.rs
|
||||
descriptor/
|
||||
mod.rs
|
||||
loader.rs
|
||||
source.rs
|
||||
registry.rs
|
||||
reflect/
|
||||
mod.rs
|
||||
client.rs
|
||||
model/
|
||||
mod.rs
|
||||
service.rs
|
||||
method.rs
|
||||
message.rs
|
||||
field.rs
|
||||
convert/
|
||||
mod.rs
|
||||
to_schema.rs
|
||||
to_json.rs
|
||||
from_json.rs
|
||||
errors.rs
|
||||
```
|
||||
|
||||
Описание:
|
||||
|
||||
- `descriptor/loader.rs` - загрузка descriptor set.
|
||||
- `descriptor/source.rs` - типы источников: upload, file, reflection.
|
||||
- `descriptor/registry.rs` - индексирование описаний для поиска services/methods.
|
||||
- `reflect/client.rs` - клиент server reflection, если будет добавлен.
|
||||
- `model/*` - protobuf-ориентированная промежуточная модель.
|
||||
- `convert/to_schema.rs` - перевод protobuf message в `crank-schema`.
|
||||
- `convert/to_json.rs` и `from_json.rs` - преобразование runtime payload.
|
||||
|
||||
Почему отдельный crate:
|
||||
|
||||
protobuf-логика объемная и быстро начнет загрязнять gRPC adapter, если не отделить ее сразу.
|
||||
|
||||
### 4.5. `crank-registry`
|
||||
|
||||
Назначение:
|
||||
|
||||
- хранение операций, схем, descriptor links и статусов;
|
||||
- выдача draft/published представлений;
|
||||
- поиск активных операций для runtime и MCP server.
|
||||
|
||||
Структура:
|
||||
|
||||
```text
|
||||
crank-registry/
|
||||
src/
|
||||
lib.rs
|
||||
model/
|
||||
mod.rs
|
||||
record.rs
|
||||
draft.rs
|
||||
published.rs
|
||||
repo/
|
||||
mod.rs
|
||||
operation_repo.rs
|
||||
descriptor_repo.rs
|
||||
service/
|
||||
mod.rs
|
||||
create_operation.rs
|
||||
update_operation.rs
|
||||
publish_operation.rs
|
||||
list_operations.rs
|
||||
get_runtime_view.rs
|
||||
storage/
|
||||
mod.rs
|
||||
postgres.rs
|
||||
cache/
|
||||
mod.rs
|
||||
runtime_cache.rs
|
||||
errors.rs
|
||||
```
|
||||
|
||||
Описание:
|
||||
|
||||
- `model/record.rs` - DB-aligned record model.
|
||||
- `model/draft.rs` - модель черновика.
|
||||
- `model/published.rs` - модель опубликованной операции.
|
||||
- `repo/*` - контракты репозиториев.
|
||||
- `service/*` - use case операции над реестром.
|
||||
- `storage/*` - реализации репозиториев на `sqlx`.
|
||||
- `cache/runtime_cache.rs` - кэш активных операций.
|
||||
|
||||
Правило:
|
||||
|
||||
`registry` не выполняет операции и не знает о `reqwest`/`tonic`. Он только хранит и отдает согласованные представления.
|
||||
- хранение workspace-scoped operations и version snapshots;
|
||||
- хранение agents и agent versions;
|
||||
- auth profiles;
|
||||
- platform API keys;
|
||||
- logs и usage aggregates;
|
||||
- metadata по sample artifacts и descriptors.
|
||||
|
||||
### 4.6. `crank-runtime`
|
||||
|
||||
Назначение:
|
||||
|
||||
- исполнение операций;
|
||||
- orchestration между схемой, mapping и адаптерами;
|
||||
- выдача нормализованного результата.
|
||||
- исполнение published operation;
|
||||
- запись invocation events;
|
||||
- возврат нормализованного результата.
|
||||
|
||||
Структура:
|
||||
### 4.7. Protocol adapters
|
||||
|
||||
```text
|
||||
crank-runtime/
|
||||
src/
|
||||
lib.rs
|
||||
executor/
|
||||
mod.rs
|
||||
operation_executor.rs
|
||||
input_prepare.rs
|
||||
output_finalize.rs
|
||||
adapter/
|
||||
mod.rs
|
||||
traits.rs
|
||||
dispatch.rs
|
||||
context/
|
||||
mod.rs
|
||||
execution_context.rs
|
||||
model/
|
||||
mod.rs
|
||||
runtime_operation.rs
|
||||
prepared_request.rs
|
||||
adapter_response.rs
|
||||
errors.rs
|
||||
```
|
||||
- `crank-adapter-rest`
|
||||
- `crank-adapter-graphql`
|
||||
- `crank-adapter-grpc`
|
||||
|
||||
Описание:
|
||||
Каждый adapter знает только свой протокол.
|
||||
|
||||
- `executor/operation_executor.rs` - основной orchestration use case.
|
||||
- `executor/input_prepare.rs` - валидация входа и применение input mapping.
|
||||
- `executor/output_finalize.rs` - обработка adapter response и output mapping.
|
||||
- `adapter/traits.rs` - общий контракт для протокольных адаптеров.
|
||||
- `adapter/dispatch.rs` - выбор адаптера по протоколу.
|
||||
- `context/execution_context.rs` - correlation id, deadlines, tracing data.
|
||||
- `model/runtime_operation.rs` - runtime-ready представление операции.
|
||||
### 4.8. `apps/admin-api`
|
||||
|
||||
Правило:
|
||||
Должен содержать сервисные группы:
|
||||
|
||||
`runtime` не должен знать, где хранится операция. Он получает уже готовую `runtime_operation`.
|
||||
- `workspaces`
|
||||
- `memberships`
|
||||
- `operations`
|
||||
- `auth_profiles`
|
||||
- `agents`
|
||||
- `platform_api_keys`
|
||||
- `logs`
|
||||
- `usage`
|
||||
|
||||
### 4.7. `crank-adapter-rest`
|
||||
### 4.9. `apps/mcp-server`
|
||||
|
||||
Назначение:
|
||||
|
||||
- построение и выполнение REST-вызовов.
|
||||
- публикация published agent bindings как MCP tools;
|
||||
- transport handling;
|
||||
- JSON-RPC lifecycle;
|
||||
- вызов runtime.
|
||||
|
||||
Структура:
|
||||
Антипаттерн:
|
||||
|
||||
```text
|
||||
crank-adapter-rest/
|
||||
src/
|
||||
lib.rs
|
||||
client.rs
|
||||
request/
|
||||
mod.rs
|
||||
build.rs
|
||||
path.rs
|
||||
query.rs
|
||||
headers.rs
|
||||
body.rs
|
||||
response/
|
||||
mod.rs
|
||||
decode.rs
|
||||
normalize.rs
|
||||
errors.rs
|
||||
```
|
||||
|
||||
Правило:
|
||||
|
||||
REST adapter не валидирует MCP input и не знает о registry. Он получает уже подготовленный request contract.
|
||||
|
||||
### 4.8. `crank-adapter-graphql`
|
||||
|
||||
Назначение:
|
||||
|
||||
- построение и выполнение GraphQL-вызовов.
|
||||
|
||||
Структура:
|
||||
|
||||
```text
|
||||
crank-adapter-graphql/
|
||||
src/
|
||||
lib.rs
|
||||
client.rs
|
||||
request/
|
||||
mod.rs
|
||||
build.rs
|
||||
variables.rs
|
||||
response/
|
||||
mod.rs
|
||||
decode.rs
|
||||
extract.rs
|
||||
errors.rs
|
||||
```
|
||||
|
||||
Правило:
|
||||
|
||||
GraphQL adapter не занимается introspection по умолчанию и не содержит редактор схем. Он только исполняет подготовленный operation template.
|
||||
|
||||
Дополнительное ограничение:
|
||||
|
||||
один GraphQL tool соответствует одному заранее определенному `query` или `mutation`. Адаптер не должен принимать от LLM произвольный GraphQL-документ, потому что в MCP-модели операция должна оставаться узкой, предсказуемой и валидируемой по фиксированной схеме.
|
||||
|
||||
### 4.9. `crank-adapter-grpc`
|
||||
|
||||
Назначение:
|
||||
|
||||
- выполнение unary gRPC-вызовов на основе уже выбранного метода и descriptor metadata.
|
||||
|
||||
Структура:
|
||||
|
||||
```text
|
||||
crank-adapter-grpc/
|
||||
src/
|
||||
lib.rs
|
||||
channel.rs
|
||||
invoke/
|
||||
mod.rs
|
||||
unary.rs
|
||||
request/
|
||||
mod.rs
|
||||
build.rs
|
||||
response/
|
||||
mod.rs
|
||||
decode.rs
|
||||
errors.rs
|
||||
```
|
||||
|
||||
Правило:
|
||||
|
||||
gRPC adapter не должен сам парсить `.proto`. Этим занимается `crank-proto`. Иначе в адаптере смешаются discovery и execution.
|
||||
|
||||
Дополнительное ограничение:
|
||||
|
||||
adapter поддерживает только unary RPC. Streaming-вызовы не реализуются, потому что целевая модель MCP tool в проекте соответствует сценарию `запрос -> ответ`, а не долгоживущей сессии обмена сообщениями.
|
||||
|
||||
### 4.10. `admin-api`
|
||||
|
||||
Назначение:
|
||||
|
||||
- HTTP API для UI;
|
||||
- координация use case создания, редактирования, тестирования и публикации.
|
||||
|
||||
Структура:
|
||||
|
||||
```text
|
||||
apps/admin-api/
|
||||
src/
|
||||
main.rs
|
||||
app.rs
|
||||
state.rs
|
||||
router.rs
|
||||
http/
|
||||
mod.rs
|
||||
dto/
|
||||
mod.rs
|
||||
operation.rs
|
||||
mapping.rs
|
||||
test_run.rs
|
||||
handlers/
|
||||
mod.rs
|
||||
create_operation.rs
|
||||
update_operation.rs
|
||||
publish_operation.rs
|
||||
list_operations.rs
|
||||
test_operation.rs
|
||||
export_operation_yaml.rs
|
||||
import_operation_yaml.rs
|
||||
upload_json_samples.rs
|
||||
upload_proto.rs
|
||||
list_grpc_services.rs
|
||||
response.rs
|
||||
services/
|
||||
mod.rs
|
||||
operation_service.rs
|
||||
descriptor_service.rs
|
||||
config_portability_service.rs
|
||||
test_service.rs
|
||||
errors.rs
|
||||
```
|
||||
|
||||
Правила:
|
||||
|
||||
- HTTP DTO не должны протекать в доменный слой;
|
||||
- handlers должны быть тонкими;
|
||||
- orchestration должна жить в `services/*`;
|
||||
- `state.rs` не должен разрастаться в огромную структуру. Лучше использовать вложенные state-компоненты или отдельные service bundles.
|
||||
- import/export конфигурации в `YAML` должен быть отдельным use case, а не побочным эффектом обычного CRUD.
|
||||
|
||||
### 4.11. `mcp-server`
|
||||
|
||||
Назначение:
|
||||
|
||||
- публикация MCP tools;
|
||||
- вызов runtime по имени tool;
|
||||
- обновление активного списка tools.
|
||||
|
||||
Структура:
|
||||
|
||||
```text
|
||||
apps/mcp-server/
|
||||
src/
|
||||
main.rs
|
||||
app.rs
|
||||
state.rs
|
||||
tools/
|
||||
mod.rs
|
||||
list.rs
|
||||
call.rs
|
||||
cache.rs
|
||||
translate/
|
||||
mod.rs
|
||||
to_mcp_tool.rs
|
||||
from_mcp_input.rs
|
||||
to_mcp_output.rs
|
||||
errors.rs
|
||||
```
|
||||
|
||||
Правило:
|
||||
|
||||
`mcp-server` не должен реализовывать business rules публикации. Он читает уже опубликованные операции и транслирует их в MCP.
|
||||
|
||||
### 4.12. `ui`
|
||||
|
||||
Назначение:
|
||||
|
||||
- интерфейс оператора.
|
||||
|
||||
Предлагаемая frontend-структура:
|
||||
|
||||
```text
|
||||
apps/ui/
|
||||
src/
|
||||
main.tsx
|
||||
app/
|
||||
router.tsx
|
||||
providers.tsx
|
||||
pages/
|
||||
operation-list/
|
||||
operation-create/
|
||||
operation-edit/
|
||||
operation-test/
|
||||
grpc-browser/
|
||||
features/
|
||||
operation-form/
|
||||
mapping-editor/
|
||||
sample-upload/
|
||||
grpc-method-picker/
|
||||
schema-viewer/
|
||||
publish-operation/
|
||||
entities/
|
||||
operation/
|
||||
descriptor/
|
||||
shared/
|
||||
api/
|
||||
lib/
|
||||
ui/
|
||||
config/
|
||||
```
|
||||
|
||||
Правило:
|
||||
|
||||
UI должен декомпозироваться по пользовательским сценариям, а не по типам файлов уровня "все компоненты в одной папке".
|
||||
|
||||
## 5. Правила зависимостей между crate
|
||||
|
||||
Целевой граф зависимостей:
|
||||
|
||||
```text
|
||||
crank-core
|
||||
crank-schema -> crank-core
|
||||
crank-mapping -> crank-core
|
||||
crank-proto -> crank-core, crank-schema
|
||||
crank-registry -> crank-core, crank-schema, crank-mapping
|
||||
crank-adapter-rest -> crank-core
|
||||
crank-adapter-graphql -> crank-core
|
||||
crank-adapter-grpc -> crank-core, crank-proto
|
||||
crank-runtime -> crank-core, crank-schema, crank-mapping, adapters
|
||||
admin-api -> crank-core, crank-schema, crank-mapping, crank-proto, crank-registry, crank-runtime
|
||||
mcp-server -> crank-core, crank-registry, crank-runtime
|
||||
```
|
||||
|
||||
Критические ограничения:
|
||||
|
||||
- `crank-core` ни от кого не зависит;
|
||||
- адаптеры не зависят от `registry`;
|
||||
- `runtime` не зависит от `admin-api` и `mcp-server`;
|
||||
- `registry` не зависит от адаптеров;
|
||||
- `ui` зависит только от HTTP API.
|
||||
|
||||
## 6. Границы публичных API модулей
|
||||
|
||||
Чтобы структура не разъехалась, нужно заранее ограничить публичность.
|
||||
|
||||
Рекомендуемое правило:
|
||||
|
||||
- наружу экспортируются только корневые доменные типы, service-интерфейсы и ошибки;
|
||||
- внутренние DTO, record-модели и промежуточные builder-структуры остаются `pub(crate)`;
|
||||
- не реэкспортировать целые деревья модулей без необходимости;
|
||||
- не делать `mod utils`, если можно назвать модуль по смыслу.
|
||||
|
||||
Пример плохого решения:
|
||||
|
||||
- `pub mod common;`
|
||||
- `pub mod helpers;`
|
||||
- `pub struct AppContext { ... 25 полей ... }`
|
||||
|
||||
Пример правильного решения:
|
||||
|
||||
- `pub struct OperationExecutor`
|
||||
- `pub trait OperationRepository`
|
||||
- `pub struct RuntimeOperation`
|
||||
|
||||
## 7. Какие большие структуры точно не нужны
|
||||
|
||||
Ниже список сущностей, которые легко превращаются в антипаттерн:
|
||||
|
||||
- одна гигантская `Operation`, содержащая сразу все REST, GraphQL и gRPC поля;
|
||||
- один `MappingConfig`, содержащий и input, и output, и transforms, и validation rules без разделения;
|
||||
- единый `AppState` со всеми репозиториями, клиентами, кэшами и конфигами;
|
||||
- один `ProtocolAdapter` с ветвлением `match protocol` внутри на сотни строк;
|
||||
- один `SchemaField` без выделения object/array/enum/oneof вариантов.
|
||||
|
||||
Правильный подход:
|
||||
|
||||
- отдельные target-типы по протоколам;
|
||||
- отдельные input/output mapping модели;
|
||||
- отдельные bounded state-наборы для каждого приложения;
|
||||
- отдельные adapter crates;
|
||||
- выделенная иерархия schema types.
|
||||
|
||||
## 8. Порядок реализации без архитектурного долга
|
||||
|
||||
Рекомендуемый порядок разработки:
|
||||
|
||||
1. `crank-core`
|
||||
2. `crank-schema`
|
||||
3. `crank-mapping`
|
||||
4. `crank-registry`
|
||||
5. `crank-adapter-rest`
|
||||
6. `crank-runtime`
|
||||
7. `admin-api`
|
||||
8. `ui`
|
||||
9. `crank-proto`
|
||||
10. `crank-adapter-grpc`
|
||||
11. `crank-adapter-graphql`
|
||||
12. `mcp-server`
|
||||
|
||||
Причина такого порядка:
|
||||
|
||||
- сначала фиксируется доменная модель;
|
||||
- затем схема и mapping как самые чувствительные части;
|
||||
- затем реестр и базовое выполнение REST;
|
||||
- после этого можно собирать UI и только потом наращивать сложные протоколы.
|
||||
|
||||
## 9. Практический итог
|
||||
|
||||
Если придерживаться этой декомпозиции, то:
|
||||
|
||||
- `crank-core` останется маленьким и стабильным;
|
||||
- schema и mapping не смешаются с transport-логикой;
|
||||
- protobuf discovery не загрязнит gRPC runtime;
|
||||
- `admin-api` и `mcp-server` останутся тонкими входными слоями;
|
||||
- добавление нового протокола не потребует переписывать половину проекта.
|
||||
|
||||
Это и есть целевая архитектурная дисциплина проекта: отдельные слои, отдельные модели, минимально необходимая публичность и отсутствие "универсальных" структур, в которые со временем начинает стекаться вся система.
|
||||
не превращать `mcp-server` во второй `admin-api`.
|
||||
|
||||
@@ -0,0 +1,677 @@
|
||||
# Operations Workspace Contracts
|
||||
|
||||
## 1. Назначение документа
|
||||
|
||||
Этот документ задает точные `workspace-scoped` контракты для экранов:
|
||||
|
||||
- `Operations`
|
||||
- `Wizard`
|
||||
|
||||
Документ нужен как промежуточный слой между:
|
||||
|
||||
- целевым UI в `test-ui`;
|
||||
- `docs/backend-gap-plan.md`;
|
||||
- будущей реализацией `admin-api`.
|
||||
|
||||
## 2. Общие правила
|
||||
|
||||
Базовый префикс:
|
||||
|
||||
```text
|
||||
/api/admin/workspaces/{workspace_id}
|
||||
```
|
||||
|
||||
Общие принципы:
|
||||
|
||||
- все operation принадлежат одному workspace;
|
||||
- каталог операций отдается целиком с сервера и не требует client-side merge поверх локального mock state;
|
||||
- wizard работает только через backend и не опирается на `localStorage` или `sessionStorage`;
|
||||
- versioning остается явным;
|
||||
- текущий draft lifecycle отделен от published lifecycle;
|
||||
- `PATCH /operations/{operation_id}` обновляет текущий draft;
|
||||
- `POST /operations/{operation_id}/versions` создает новый explicit snapshot;
|
||||
- опубликованная operation не удаляется hard delete, если на нее есть published agent bindings.
|
||||
|
||||
## 3. Канонические представления
|
||||
|
||||
### 3.1. `OperationSummary`
|
||||
|
||||
Используется в каталоге операций.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "op_01hr9f7q4b0w1svxv5a6j8k2np",
|
||||
"workspace_id": "ws_01hr9dzv4s5ec1f1v3y8t1n7gr",
|
||||
"name": "crm_create_lead",
|
||||
"display_name": "Create Lead",
|
||||
"protocol": "rest",
|
||||
"status": "draft",
|
||||
"category": "sales",
|
||||
"current_draft_version": 3,
|
||||
"latest_published_version": 2,
|
||||
"updated_at": "2026-03-29T12:00:00Z",
|
||||
"usage_summary": {
|
||||
"calls_today": 4821,
|
||||
"error_rate_pct": 1.8,
|
||||
"avg_latency_ms": 187
|
||||
},
|
||||
"agent_refs": [
|
||||
{
|
||||
"agent_id": "agent_01hr9g3kgznn57d8s1q0h0g7qn",
|
||||
"agent_slug": "sales-assistant",
|
||||
"display_name": "Sales Assistant"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2. `OperationDetail`
|
||||
|
||||
Используется на detail page и как источник для edit-mode wizard.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "op_01hr9f7q4b0w1svxv5a6j8k2np",
|
||||
"workspace_id": "ws_01hr9dzv4s5ec1f1v3y8t1n7gr",
|
||||
"name": "crm_create_lead",
|
||||
"display_name": "Create Lead",
|
||||
"protocol": "rest",
|
||||
"status": "draft",
|
||||
"category": "sales",
|
||||
"current_draft_version": 3,
|
||||
"latest_published_version": 2,
|
||||
"published_at": "2026-03-28T18:10:00Z",
|
||||
"draft_version_ref": {
|
||||
"version": 3,
|
||||
"status": "draft"
|
||||
},
|
||||
"published_version_ref": {
|
||||
"version": 2,
|
||||
"status": "published"
|
||||
},
|
||||
"agent_refs": [
|
||||
{
|
||||
"agent_id": "agent_01hr9g3kgznn57d8s1q0h0g7qn",
|
||||
"agent_slug": "sales-assistant",
|
||||
"display_name": "Sales Assistant"
|
||||
}
|
||||
],
|
||||
"created_at": "2026-03-27T09:15:00Z",
|
||||
"updated_at": "2026-03-29T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3. `OperationVersionDocument`
|
||||
|
||||
Полная конфигурация версии. Используется wizard и detail page.
|
||||
|
||||
```json
|
||||
{
|
||||
"operation_id": "op_01hr9f7q4b0w1svxv5a6j8k2np",
|
||||
"workspace_id": "ws_01hr9dzv4s5ec1f1v3y8t1n7gr",
|
||||
"version": 3,
|
||||
"status": "draft",
|
||||
"target": {},
|
||||
"input_schema": {},
|
||||
"output_schema": {},
|
||||
"input_mapping": {},
|
||||
"output_mapping": {},
|
||||
"execution_config": {},
|
||||
"tool_description": {},
|
||||
"samples": {
|
||||
"input_json": {},
|
||||
"output_json": {}
|
||||
},
|
||||
"generated_draft": {
|
||||
"input_schema": {},
|
||||
"output_schema": {},
|
||||
"input_mapping": {},
|
||||
"output_mapping": {}
|
||||
},
|
||||
"config_export": {
|
||||
"format_version": "1",
|
||||
"export_mode": "portable"
|
||||
},
|
||||
"change_note": "update request mapping",
|
||||
"created_at": "2026-03-29T12:00:00Z",
|
||||
"updated_at": "2026-03-29T12:45:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4. `OperationMutationResult`
|
||||
|
||||
Используется как ответ на create, patch, explicit version, archive и delete.
|
||||
|
||||
```json
|
||||
{
|
||||
"operation_id": "op_01hr9f7q4b0w1svxv5a6j8k2np",
|
||||
"workspace_id": "ws_01hr9dzv4s5ec1f1v3y8t1n7gr",
|
||||
"version": 3,
|
||||
"status": "draft",
|
||||
"updated_at": "2026-03-29T12:45:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## 4. Catalog endpoints
|
||||
|
||||
### `GET /api/admin/workspaces/{workspace_id}/operations`
|
||||
|
||||
Назначение:
|
||||
|
||||
- отдать каталог операций для списка.
|
||||
|
||||
Query params:
|
||||
|
||||
- `protocol`
|
||||
- `status`
|
||||
- `search`
|
||||
- `category`
|
||||
- `agent_id`
|
||||
- `page`
|
||||
- `page_size`
|
||||
|
||||
Ответ:
|
||||
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"id": "op_01hr9f7q4b0w1svxv5a6j8k2np",
|
||||
"workspace_id": "ws_01hr9dzv4s5ec1f1v3y8t1n7gr",
|
||||
"name": "crm_create_lead",
|
||||
"display_name": "Create Lead",
|
||||
"protocol": "rest",
|
||||
"status": "draft",
|
||||
"category": "sales",
|
||||
"current_draft_version": 3,
|
||||
"latest_published_version": 2,
|
||||
"updated_at": "2026-03-29T12:00:00Z",
|
||||
"usage_summary": {
|
||||
"calls_today": 4821,
|
||||
"error_rate_pct": 1.8,
|
||||
"avg_latency_ms": 187
|
||||
},
|
||||
"agent_refs": []
|
||||
}
|
||||
],
|
||||
"page": 1,
|
||||
"page_size": 20,
|
||||
"total": 48
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /api/admin/workspaces/{workspace_id}/operations/{operation_id}`
|
||||
|
||||
Назначение:
|
||||
|
||||
- отдать `OperationDetail`.
|
||||
|
||||
### `GET /api/admin/workspaces/{workspace_id}/operations/{operation_id}/versions/{version}`
|
||||
|
||||
Назначение:
|
||||
|
||||
- отдать `OperationVersionDocument`.
|
||||
|
||||
## 5. Create and update contracts
|
||||
|
||||
### `POST /api/admin/workspaces/{workspace_id}/operations`
|
||||
|
||||
Назначение:
|
||||
|
||||
- создать operation и draft version `1`.
|
||||
|
||||
Тело:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "crm_create_lead",
|
||||
"display_name": "Create Lead",
|
||||
"protocol": "rest",
|
||||
"category": "sales",
|
||||
"target": {},
|
||||
"input_schema": {},
|
||||
"output_schema": {},
|
||||
"input_mapping": {},
|
||||
"output_mapping": {},
|
||||
"execution_config": {},
|
||||
"tool_description": {}
|
||||
}
|
||||
```
|
||||
|
||||
Ответ:
|
||||
|
||||
```json
|
||||
{
|
||||
"operation_id": "op_01hr9f7q4b0w1svxv5a6j8k2np",
|
||||
"workspace_id": "ws_01hr9dzv4s5ec1f1v3y8t1n7gr",
|
||||
"version": 1,
|
||||
"status": "draft",
|
||||
"updated_at": "2026-03-29T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### `PATCH /api/admin/workspaces/{workspace_id}/operations/{operation_id}`
|
||||
|
||||
Назначение:
|
||||
|
||||
- обновить identity metadata;
|
||||
- обновить текущий draft document без создания новой версии вручную со стороны UI.
|
||||
|
||||
Это endpoint для обычного wizard edit-mode.
|
||||
|
||||
Тело:
|
||||
|
||||
```json
|
||||
{
|
||||
"display_name": "Create Lead",
|
||||
"category": "sales",
|
||||
"draft_document": {
|
||||
"target": {},
|
||||
"input_schema": {},
|
||||
"output_schema": {},
|
||||
"input_mapping": {},
|
||||
"output_mapping": {},
|
||||
"execution_config": {},
|
||||
"tool_description": {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Ответ:
|
||||
|
||||
```json
|
||||
{
|
||||
"operation_id": "op_01hr9f7q4b0w1svxv5a6j8k2np",
|
||||
"workspace_id": "ws_01hr9dzv4s5ec1f1v3y8t1n7gr",
|
||||
"version": 3,
|
||||
"status": "draft",
|
||||
"updated_at": "2026-03-29T12:45:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### `POST /api/admin/workspaces/{workspace_id}/operations/{operation_id}/versions`
|
||||
|
||||
Назначение:
|
||||
|
||||
- создать новую explicit draft-version snapshot.
|
||||
|
||||
Этот endpoint нужен для сценариев:
|
||||
|
||||
- `Save as new version`;
|
||||
- controlled version history;
|
||||
- YAML import с `mode=upsert`.
|
||||
|
||||
Тело:
|
||||
|
||||
```json
|
||||
{
|
||||
"source_version": 3,
|
||||
"change_note": "prepare release candidate",
|
||||
"document": {
|
||||
"target": {},
|
||||
"input_schema": {},
|
||||
"output_schema": {},
|
||||
"input_mapping": {},
|
||||
"output_mapping": {},
|
||||
"execution_config": {},
|
||||
"tool_description": {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Ответ:
|
||||
|
||||
```json
|
||||
{
|
||||
"operation_id": "op_01hr9f7q4b0w1svxv5a6j8k2np",
|
||||
"workspace_id": "ws_01hr9dzv4s5ec1f1v3y8t1n7gr",
|
||||
"version": 4,
|
||||
"status": "draft",
|
||||
"updated_at": "2026-03-29T13:05:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## 6. Lifecycle endpoints
|
||||
|
||||
### `POST /api/admin/workspaces/{workspace_id}/operations/{operation_id}/publish`
|
||||
|
||||
Назначение:
|
||||
|
||||
- опубликовать конкретную версию операции.
|
||||
|
||||
Тело:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 3
|
||||
}
|
||||
```
|
||||
|
||||
Ответ:
|
||||
|
||||
```json
|
||||
{
|
||||
"operation_id": "op_01hr9f7q4b0w1svxv5a6j8k2np",
|
||||
"workspace_id": "ws_01hr9dzv4s5ec1f1v3y8t1n7gr",
|
||||
"version": 3,
|
||||
"status": "published",
|
||||
"updated_at": "2026-03-29T13:10:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### `POST /api/admin/workspaces/{workspace_id}/operations/{operation_id}/archive`
|
||||
|
||||
Назначение:
|
||||
|
||||
- перевести operation в archived lifecycle state без hard delete.
|
||||
|
||||
Тело:
|
||||
|
||||
```json
|
||||
{
|
||||
"reason": "deprecated by crm_create_lead_v2"
|
||||
}
|
||||
```
|
||||
|
||||
Ответ:
|
||||
|
||||
```json
|
||||
{
|
||||
"operation_id": "op_01hr9f7q4b0w1svxv5a6j8k2np",
|
||||
"workspace_id": "ws_01hr9dzv4s5ec1f1v3y8t1n7gr",
|
||||
"version": 3,
|
||||
"status": "archived",
|
||||
"updated_at": "2026-03-29T13:15:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### `DELETE /api/admin/workspaces/{workspace_id}/operations/{operation_id}`
|
||||
|
||||
Назначение:
|
||||
|
||||
- hard delete для еще не опубликованной operation.
|
||||
|
||||
Правила:
|
||||
|
||||
- hard delete допустим только для draft/unpublished operation;
|
||||
- если operation уже публиковалась, UI должен использовать archive;
|
||||
- если operation используется published agent-ами, endpoint возвращает `409 Conflict`.
|
||||
|
||||
Ответ:
|
||||
|
||||
```json
|
||||
{
|
||||
"operation_id": "op_01hr9f7q4b0w1svxv5a6j8k2np",
|
||||
"workspace_id": "ws_01hr9dzv4s5ec1f1v3y8t1n7gr",
|
||||
"deleted": true
|
||||
}
|
||||
```
|
||||
|
||||
## 7. Wizard-specific endpoints
|
||||
|
||||
### `POST /api/admin/workspaces/{workspace_id}/operations/{operation_id}/test-runs`
|
||||
|
||||
Тело:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 3,
|
||||
"input": {}
|
||||
}
|
||||
```
|
||||
|
||||
Ответ:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"request_preview": {},
|
||||
"response_preview": {},
|
||||
"output": {},
|
||||
"runtime_labels": {
|
||||
"workspace_id": "ws_01hr9dzv4s5ec1f1v3y8t1n7gr",
|
||||
"operation_id": "op_01hr9f7q4b0w1svxv5a6j8k2np"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `POST /api/admin/workspaces/{workspace_id}/operations/{operation_id}/samples/input-json`
|
||||
|
||||
Тело:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 3,
|
||||
"sample": {}
|
||||
}
|
||||
```
|
||||
|
||||
Ответ:
|
||||
|
||||
```json
|
||||
{
|
||||
"operation_id": "op_01hr9f7q4b0w1svxv5a6j8k2np",
|
||||
"version": 3,
|
||||
"stored": true
|
||||
}
|
||||
```
|
||||
|
||||
### `POST /api/admin/workspaces/{workspace_id}/operations/{operation_id}/samples/output-json`
|
||||
|
||||
Тело:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 3,
|
||||
"sample": {}
|
||||
}
|
||||
```
|
||||
|
||||
Ответ:
|
||||
|
||||
```json
|
||||
{
|
||||
"operation_id": "op_01hr9f7q4b0w1svxv5a6j8k2np",
|
||||
"version": 3,
|
||||
"stored": true
|
||||
}
|
||||
```
|
||||
|
||||
### `POST /api/admin/workspaces/{workspace_id}/operations/{operation_id}/drafts/generate`
|
||||
|
||||
Назначение:
|
||||
|
||||
- сгенерировать черновую схему и draft mappings по samples/descriptors.
|
||||
|
||||
Тело:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 3
|
||||
}
|
||||
```
|
||||
|
||||
Ответ должен возвращать полный draft payload, пригодный для прямой подстановки в wizard:
|
||||
|
||||
```json
|
||||
{
|
||||
"generated_draft": {
|
||||
"input_schema": {},
|
||||
"output_schema": {},
|
||||
"input_mapping": {},
|
||||
"output_mapping": {}
|
||||
},
|
||||
"input_schema": {},
|
||||
"output_schema": {},
|
||||
"input_mapping": {},
|
||||
"output_mapping": {}
|
||||
}
|
||||
```
|
||||
|
||||
### `POST /api/admin/workspaces/{workspace_id}/operations/{operation_id}/descriptors/proto`
|
||||
|
||||
Тело:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 3,
|
||||
"file_name": "crm.proto",
|
||||
"content_b64": "..."
|
||||
}
|
||||
```
|
||||
|
||||
Ответ:
|
||||
|
||||
```json
|
||||
{
|
||||
"descriptor_ref": "desc_01hr9n0m2yzdb8f8xv1gvztm2b",
|
||||
"stored": true
|
||||
}
|
||||
```
|
||||
|
||||
### `POST /api/admin/workspaces/{workspace_id}/operations/{operation_id}/descriptors/descriptor-set`
|
||||
|
||||
Тело:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 3,
|
||||
"file_name": "crm-descriptor-set.bin",
|
||||
"content_b64": "..."
|
||||
}
|
||||
```
|
||||
|
||||
Ответ:
|
||||
|
||||
```json
|
||||
{
|
||||
"descriptor_ref": "desc_01hr9n0m2yzdb8f8xv1gvztm2b",
|
||||
"stored": true
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /api/admin/workspaces/{workspace_id}/operations/{operation_id}/grpc/services?version=3`
|
||||
|
||||
Ответ:
|
||||
|
||||
```json
|
||||
{
|
||||
"services": [
|
||||
{
|
||||
"package": "crm.v1",
|
||||
"service": "LeadService",
|
||||
"methods": [
|
||||
{
|
||||
"name": "CreateLead",
|
||||
"kind": "unary",
|
||||
"input_schema": {},
|
||||
"output_schema": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 8. YAML contracts
|
||||
|
||||
### `GET /api/admin/workspaces/{workspace_id}/operations/{operation_id}/export`
|
||||
|
||||
Query:
|
||||
|
||||
- `mode=portable|bundle`
|
||||
- `version=<optional>`
|
||||
|
||||
Ответ:
|
||||
|
||||
- `application/yaml`
|
||||
|
||||
### `POST /api/admin/workspaces/{workspace_id}/operations/import`
|
||||
|
||||
Query:
|
||||
|
||||
- `mode=create|upsert`
|
||||
|
||||
Body:
|
||||
|
||||
- raw YAML document
|
||||
|
||||
Ответ:
|
||||
|
||||
```json
|
||||
{
|
||||
"workspace_id": "ws_01hr9dzv4s5ec1f1v3y8t1n7gr",
|
||||
"operation_id": "op_01hr9f7q4b0w1svxv5a6j8k2np",
|
||||
"version": 4,
|
||||
"status": "draft",
|
||||
"result": "upserted"
|
||||
}
|
||||
```
|
||||
|
||||
## 9. Wizard lifecycle semantics
|
||||
|
||||
### Текущее редактирование
|
||||
|
||||
Wizard всегда открывается на current draft version и использует:
|
||||
|
||||
- `GET /operations/{operation_id}`
|
||||
- `GET /operations/{operation_id}/versions/{current_draft_version}`
|
||||
- `PATCH /operations/{operation_id}`
|
||||
|
||||
Это основной сценарий редактирования.
|
||||
|
||||
### Явное создание новой версии
|
||||
|
||||
Если UI вводит действие `Save as new version`, оно должно использовать:
|
||||
|
||||
- `POST /operations/{operation_id}/versions`
|
||||
|
||||
Это уже отдельный snapshot, а не обычное сохранение формы.
|
||||
|
||||
### Публикация
|
||||
|
||||
Публикуется конкретная version, а не “текущее состояние формы”.
|
||||
|
||||
Поэтому UI всегда должен передавать:
|
||||
|
||||
- `version`
|
||||
|
||||
в `POST /publish`.
|
||||
|
||||
## 10. Конфликты, которые закрывает этот документ
|
||||
|
||||
### Конфликт 1. `PATCH` против explicit version snapshots
|
||||
|
||||
Решение:
|
||||
|
||||
- wizard использует `PATCH` для текущего draft;
|
||||
- controlled snapshots остаются на `POST /versions`.
|
||||
|
||||
### Конфликт 2. Delete semantics
|
||||
|
||||
Решение:
|
||||
|
||||
- hard delete только для unpublished drafts;
|
||||
- для опубликованных операций использовать archive.
|
||||
|
||||
### Конфликт 3. Category source of truth
|
||||
|
||||
Решение:
|
||||
|
||||
- `category` признается частью operation identity metadata и хранится на стороне backend.
|
||||
|
||||
### Конфликт 4. Catalog data merge
|
||||
|
||||
Решение:
|
||||
|
||||
- каталог должен возвращать все нужные поля для UI с сервера;
|
||||
- локальные overlays и tombstones в целевой реализации не используются.
|
||||
|
||||
## 11. Следующий шаг
|
||||
|
||||
После этого документа следующая реализация должна идти в таком порядке:
|
||||
|
||||
1. обновить `admin-api` handlers и service contracts под workspace prefix;
|
||||
2. добавить `category`, `workspace_id` и update/delete/archive lifecycle в storage model;
|
||||
3. перевести wizard на create/patch/version semantics из этого документа;
|
||||
4. только после этого подключать `Operations` и `Wizard` к реальному UI.
|
||||
Reference in New Issue
Block a user