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

|

|
||||||
|
|
||||||
Crank - это low-code платформа для публикации внешних API в виде MCP tools без написания нового backend-обработчика под каждую интеграцию. Система предоставляет единый административный UI, в котором оператор может подключать REST, GraphQL и gRPC операции, настраивать маппинг входных и выходных данных, выполнять тестовый вызов и публиковать результат как MCP tool.
|
Crank - платформа для публикации внешних API в виде MCP tools без написания отдельного backend-кода под каждую интеграцию. Целевая модель проекта строится вокруг связки `workspace -> agent -> operations`.
|
||||||
|
|
||||||
На текущем этапе репозиторий содержит проектную документацию и архитектурные решения, которые задают границы MVP и подход к реализации.
|
|
||||||
|
|
||||||
## Цели
|
## Цели
|
||||||
|
|
||||||
@@ -12,75 +10,80 @@ Crank - это low-code платформа для публикации внеш
|
|||||||
- Поддержать динамическое добавление интеграций через UI или конфигурацию.
|
- Поддержать динамическое добавление интеграций через UI или конфигурацию.
|
||||||
- Обеспечить единый сценарий работы оператора для REST, GraphQL и gRPC.
|
- Обеспечить единый сценарий работы оператора для 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`.
|
- Поддержка REST для `GET`, `POST`, `PUT`, `PATCH` и `DELETE`.
|
||||||
- Поддержка GraphQL для `query` и `mutation` на основе шаблонов и переменных.
|
- Поддержка GraphQL для `query` и `mutation`.
|
||||||
- Поддержка только unary-методов gRPC.
|
- Поддержка только unary-методов gRPC.
|
||||||
- Загрузка примеров `JSON` для ускоренного создания схем и чернового маппинга.
|
- Platform API keys и membership layer.
|
||||||
- Загрузка `.proto` файлов или descriptor set для обнаружения схемы gRPC.
|
- Observability: invocation logs, usage aggregates, latency/error metrics.
|
||||||
- Импорт и экспорт конфигураций операций в `YAML`.
|
- Импорт и экспорт operation-конфигураций в `YAML`.
|
||||||
- Использование `JSONPath` для точечного маппинга вложенных параметров и ответа.
|
- Использование `JSONPath` для точечного маппинга.
|
||||||
- Настройка маппинга запроса и ответа через UI.
|
|
||||||
- Публикация tools в MCP без пересборки backend.
|
|
||||||
|
|
||||||
## Структура документации
|
## Структура документации
|
||||||
|
|
||||||
- `docs/architecture.md` - архитектура системы, модули, потоки данных и стек.
|
- `docs/architecture.md` - целевая архитектура системы.
|
||||||
- `docs/module-decomposition.md` - детальная декомпозиция crates и внутренних модулей.
|
- `docs/as-is-to-be.md` - переход `as is -> to be`, page-by-page gap analysis и архитектурные конфликты.
|
||||||
- `docs/data-model.md` - формальная модель данных и JSON-структуры сущностей.
|
- `docs/backend-gap-plan.md` - конкретный backend-план: сущности, API, БД и порядок реализации.
|
||||||
- `docs/database-schema.md` - схема БД, связи и versioning конфигураций.
|
- `docs/operations-workspace-contracts.md` - точные `workspace-scoped` контракты для экранов `Operations` и `Wizard`.
|
||||||
- `docs/admin-api.md` - HTTP-контракты административного API.
|
- `docs/module-decomposition.md` - декомпозиция crates и модулей.
|
||||||
- `docs/diagrams.md` - структурные диаграммы компонентов, сущностей, БД и потоков.
|
- `docs/data-model.md` - целевая модель данных.
|
||||||
- `docs/mcp-interface.md` - транспорт и контракт публикации MCP tools.
|
- `docs/database-schema.md` - целевая схема БД.
|
||||||
- `docs/testing-strategy.md` - стратегия тестирования до и во время разработки.
|
- `docs/admin-api.md` - целевые HTTP-контракты административного API.
|
||||||
- `docs/runtime-config.md` - конфигурация окружения, storage и секретов.
|
- `docs/diagrams.md` - диаграммы компонентов, сущностей и БД.
|
||||||
- `docs/deployment.md` - контейнерный деплой, reverse proxy и CI/CD.
|
- `docs/mcp-interface.md` - модель MCP transport и agent-scoped publishing.
|
||||||
- `docs/demo-runbook.md` - пошаговый сценарий локального запуска и воспроизводимого демо.
|
- `docs/testing-strategy.md` - стратегия тестирования.
|
||||||
- `docs/rust-design.md` - распределение методов, `impl`, `trait` и service-слоя без god-struct.
|
- `docs/runtime-config.md` - конфигурация окружения.
|
||||||
- `docs/development-rules.md` - правила разработки, TDD-процесс и git workflow.
|
- `docs/deployment.md` - деплой, reverse proxy и CI/CD.
|
||||||
- `docs/rust-code-rules.md` - Rust-specific правила кода, toolchain и linting.
|
- `docs/demo-runbook.md` - демонстрационный сценарий.
|
||||||
- `docs/implementation-plan.md` - последовательность модулей и фич по этапам реализации.
|
- `docs/rust-design.md` - правила распределения поведения в Rust.
|
||||||
- `docs/protocols/rest.md` - функциональные требования и ограничения для REST.
|
- `docs/development-rules.md` - правила разработки и workflow.
|
||||||
- `docs/protocols/graphql.md` - функциональные требования и ограничения для GraphQL.
|
- `docs/rust-code-rules.md` - Rust-specific coding rules.
|
||||||
- `docs/protocols/grpc.md` - функциональные требования и ограничения для gRPC.
|
- `docs/implementation-plan.md` - порядок перехода от текущего состояния к целевой модели.
|
||||||
|
- `docs/protocols/rest.md` - требования и ограничения для REST.
|
||||||
|
- `docs/protocols/graphql.md` - требования и ограничения для GraphQL.
|
||||||
|
- `docs/protocols/grpc.md` - требования и ограничения для gRPC.
|
||||||
|
|
||||||
## Ключевая идея продукта
|
## Ключевая идея продукта
|
||||||
|
|
||||||
Система строится вокруг унифицированной сущности `Operation`. Каждая операция описывает:
|
Система строится вокруг трех уровней:
|
||||||
|
|
||||||
- внешний протокол,
|
- `Workspace` - граница данных и доступа команды.
|
||||||
- целевой endpoint или метод,
|
- `Agent` - curated MCP endpoint для конкретного сценария LLM.
|
||||||
- входную схему,
|
- `Operation` - низкоуровневый интеграционный контракт.
|
||||||
- правила маппинга входных данных,
|
|
||||||
- параметры выполнения,
|
`Operation` описывает:
|
||||||
- правила маппинга выходных данных,
|
|
||||||
|
- внешний протокол;
|
||||||
|
- целевой endpoint или метод;
|
||||||
|
- входную схему;
|
||||||
|
- правила маппинга входных данных;
|
||||||
|
- параметры выполнения;
|
||||||
|
- правила маппинга выходных данных;
|
||||||
- метаданные MCP tool.
|
- метаданные MCP tool.
|
||||||
|
|
||||||
За счет этого MCP runtime работает с единой внутренней моделью, а протокольные адаптеры уже выполняют конкретные вызовы REST, GraphQL или gRPC.
|
`Agent` собирает ограниченный набор опубликованных операций в одну MCP-поверхность. Именно это решает проблему, когда один агент теряется в слишком большом наборе tools.
|
||||||
|
|
||||||
Для GraphQL это означает, что в MCP публикуется не "универсальный GraphQL endpoint", а конкретная операция с фиксированным шаблоном запроса, фиксированным набором входных параметров и предсказуемой структурой ответа.
|
|
||||||
|
|
||||||
Для упрощения настройки оператор может загружать примеры входного и выходного `JSON`, а для gRPC - `.proto` или descriptor set. На основе этих артефактов система строит черновую схему и стартовый маппинг, который затем вручную уточняется через `JSONPath`.
|
|
||||||
|
|
||||||
Конфигурации операций должны импортироваться и экспортироваться в `YAML`, чтобы их можно было переносить между окружениями, хранить в git и редактировать вне UI.
|
|
||||||
|
|
||||||
## CI/CD статус
|
## CI/CD статус
|
||||||
|
|
||||||
В репозитории настроены:
|
В репозитории настроены:
|
||||||
|
|
||||||
- `CI` для Rust, UI и deployment artifacts;
|
- `CI` для Rust, UI container и deployment artifacts;
|
||||||
- `CD`, который запускается после успешного `CI` на `main` или вручную;
|
- `CD`, который запускается после успешного `CI` на `main` или вручную;
|
||||||
- containerized production-like deployment через `docker compose`.
|
- containerized deployment через `docker compose`.
|
||||||
|
|
||||||
## Поддерживаемые протоколы
|
## Поддерживаемые протоколы
|
||||||
|
|
||||||
В MVP платформа ориентируется на три основных протокольных сценария интеграции:
|
В целевой модели платформа ориентируется на:
|
||||||
|
|
||||||
- REST
|
- REST
|
||||||
- GraphQL
|
- GraphQL
|
||||||
- gRPC
|
- gRPC
|
||||||
|
|
||||||
`SOAP` сознательно не входит в MVP. Он остается актуальным для части корпоративных и государственных интеграций, но требует отдельного адаптера с поддержкой WSDL, XML Schema, SOAP envelope, namespaces и XML-oriented mapping. Для первой версии это слишком большой отдельный пласт сложности.
|
`SOAP` сознательно не входит в текущий scope.
|
||||||
|
|||||||
@@ -2,22 +2,25 @@
|
|||||||
|
|
||||||
## Current
|
## Current
|
||||||
|
|
||||||
### `feat/crank-rebrand`
|
### `feat/operations-workspace-contracts`
|
||||||
|
|
||||||
Status: completed
|
Status: completed
|
||||||
|
|
||||||
DoD:
|
DoD:
|
||||||
|
|
||||||
- проект переименован в `Crank` в документации, UI и package metadata
|
- `workspace-scoped` контракты для `Operations` и `Wizard` зафиксированы
|
||||||
- workspace и crate package names согласованы с новым брендом
|
- определены точные DTO и lifecycle semantics для operations
|
||||||
- `README.md` использует `Crank.png` как главное изображение
|
- `PATCH/DELETE/ARCHIVE` и wizard DTO shape документированы
|
||||||
- `origin` указывает на `git@github.com:bsodfather/crank.git`
|
|
||||||
- Rust workspace и UI build остаются зелеными
|
|
||||||
|
|
||||||
## Next
|
## Next
|
||||||
|
|
||||||
- `feat/demo-assets`
|
- `feat/workspace-foundation`
|
||||||
|
|
||||||
## Backlog
|
## Backlog
|
||||||
|
|
||||||
|
- `feat/workspace-foundation`
|
||||||
|
- `feat/agent-publishing`
|
||||||
|
- `feat/platform-access`
|
||||||
|
- `feat/observability-api`
|
||||||
|
- `feat/alpine-ui`
|
||||||
- `feat/demo-assets`
|
- `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
|
FROM nginx:1.27-alpine
|
||||||
|
|
||||||
COPY apps/ui/nginx.conf /etc/nginx/conf.d/default.conf
|
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
|
EXPOSE 3000
|
||||||
|
|||||||
+84
-2
@@ -4,9 +4,91 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Crank Console</title>
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<main>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<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>
|
</body>
|
||||||
</html>
|
</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. Назначение документа
|
## 1. Назначение документа
|
||||||
|
|
||||||
Этот документ фиксирует HTTP-контракты административного API, через которое UI управляет операциями, загружает артефакты, тестирует вызовы и выполняет YAML import/export.
|
Этот документ фиксирует целевые HTTP-контракты административного API, через которое UI управляет workspace, operations, agents, platform access и observability.
|
||||||
|
|
||||||
Документ задает логический контракт. Конкретные детали `axum` handlers, auth middleware и response envelope могут уточняться при реализации.
|
|
||||||
|
|
||||||
## 2. Общие правила API
|
## 2. Общие правила API
|
||||||
|
|
||||||
- все payload по умолчанию в `JSON`;
|
- все payload по умолчанию в `JSON`;
|
||||||
- import/export конфигурации используют `YAML` как payload или файл;
|
- import/export конфигурации используют `YAML`;
|
||||||
- версии operation адресуются явно;
|
- все основные ресурсы являются `workspace-scoped`;
|
||||||
- published операция - это ссылка на конкретную version;
|
- версии operation и agent адресуются явно;
|
||||||
|
- published operation и published agent - ссылки на конкретные version;
|
||||||
- ошибки валидации возвращаются отдельно от transport errors.
|
- ошибки валидации возвращаются отдельно от transport errors.
|
||||||
|
|
||||||
Базовый префикс:
|
Базовый префикс:
|
||||||
@@ -22,431 +21,158 @@
|
|||||||
|
|
||||||
## 3. Основные ресурсы
|
## 3. Основные ресурсы
|
||||||
|
|
||||||
|
- `workspaces`
|
||||||
|
- `memberships`
|
||||||
|
- `invitations`
|
||||||
- `operations`
|
- `operations`
|
||||||
- `versions`
|
- `auth-profiles`
|
||||||
|
- `agents`
|
||||||
|
- `platform-api-keys`
|
||||||
|
- `logs`
|
||||||
|
- `usage`
|
||||||
- `samples`
|
- `samples`
|
||||||
- `descriptors`
|
- `descriptors`
|
||||||
- `auth-profiles`
|
|
||||||
- `test-runs`
|
|
||||||
- `config import/export`
|
- `config import/export`
|
||||||
|
|
||||||
## 4. CRUD операций
|
## 4. Workspace-scoped routing
|
||||||
|
|
||||||
### `GET /api/admin/operations`
|
Канонический префикс для UI-driven сценариев:
|
||||||
|
|
||||||
Назначение:
|
```text
|
||||||
|
/api/admin/workspaces/{workspace_id}
|
||||||
- список операций для 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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### `POST /api/admin/operations`
|
## 5. Группы endpoints
|
||||||
|
|
||||||
Назначение:
|
### 5.1. Workspaces and members
|
||||||
|
|
||||||
- создание новой операции и версии `1`.
|
- `GET /api/admin/workspaces`
|
||||||
|
- `POST /api/admin/workspaces`
|
||||||
Тело:
|
- `GET /api/admin/workspaces/{workspace_id}`
|
||||||
|
- `PATCH /api/admin/workspaces/{workspace_id}`
|
||||||
```json
|
- `GET /api/admin/workspaces/{workspace_id}/members`
|
||||||
{
|
- `POST /api/admin/workspaces/{workspace_id}/invitations`
|
||||||
"name": "crm_create_lead",
|
- `DELETE /api/admin/workspaces/{workspace_id}/invitations/{invitation_id}`
|
||||||
"display_name": "Create Lead",
|
|
||||||
"protocol": "rest",
|
### 5.2. Operations
|
||||||
"target": {
|
|
||||||
"kind": "rest",
|
- `GET /api/admin/workspaces/{workspace_id}/operations`
|
||||||
"base_url": "https://api.example.com",
|
- `POST /api/admin/workspaces/{workspace_id}/operations`
|
||||||
"method": "POST",
|
- `GET /api/admin/workspaces/{workspace_id}/operations/{operation_id}`
|
||||||
"path_template": "/v1/leads"
|
- `PATCH /api/admin/workspaces/{workspace_id}/operations/{operation_id}`
|
||||||
},
|
- `DELETE /api/admin/workspaces/{workspace_id}/operations/{operation_id}`
|
||||||
"input_schema": { "type": "object", "fields": {} },
|
- `POST /api/admin/workspaces/{workspace_id}/operations/{operation_id}/versions`
|
||||||
"output_schema": { "type": "object", "fields": {} },
|
- `GET /api/admin/workspaces/{workspace_id}/operations/{operation_id}/versions/{version}`
|
||||||
"input_mapping": { "rules": [] },
|
- `POST /api/admin/workspaces/{workspace_id}/operations/{operation_id}/publish`
|
||||||
"output_mapping": { "rules": [] },
|
- `POST /api/admin/workspaces/{workspace_id}/operations/{operation_id}/archive`
|
||||||
"execution_config": {
|
- `POST /api/admin/workspaces/{workspace_id}/operations/{operation_id}/test-runs`
|
||||||
"timeout_ms": 10000
|
- `GET /api/admin/workspaces/{workspace_id}/operations/{operation_id}/export`
|
||||||
},
|
- `POST /api/admin/workspaces/{workspace_id}/operations/import`
|
||||||
"tool_description": {
|
|
||||||
"title": "Create CRM lead",
|
### 5.3. Samples and descriptors
|
||||||
"description": "Creates a new lead."
|
|
||||||
}
|
- `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`
|
||||||
```json
|
|
||||||
{
|
### 5.4. Upstream auth profiles
|
||||||
"operation_id": "op_01",
|
|
||||||
"version": 1,
|
- `GET /api/admin/workspaces/{workspace_id}/auth-profiles`
|
||||||
"status": "draft"
|
- `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}`
|
||||||
### `GET /api/admin/operations/{operation_id}`
|
|
||||||
|
### 5.5. Agents
|
||||||
Назначение:
|
|
||||||
|
- `GET /api/admin/workspaces/{workspace_id}/agents`
|
||||||
- получить метаданные operation и ссылки на draft/published версии.
|
- `POST /api/admin/workspaces/{workspace_id}/agents`
|
||||||
|
- `GET /api/admin/workspaces/{workspace_id}/agents/{agent_id}`
|
||||||
### `GET /api/admin/operations/{operation_id}/versions/{version}`
|
- `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`
|
||||||
### `POST /api/admin/operations/{operation_id}/versions`
|
- `DELETE /api/admin/workspaces/{workspace_id}/agents/{agent_id}/bindings/{operation_id}`
|
||||||
|
|
||||||
Назначение:
|
### 5.6. Platform API keys
|
||||||
|
|
||||||
- создать новую draft-версию на основе текущего payload.
|
- `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}`
|
||||||
- полная конфигурация operation;
|
|
||||||
- опционально `change_note`.
|
### 5.7. Observability
|
||||||
|
|
||||||
Ответ:
|
- `GET /api/admin/workspaces/{workspace_id}/logs`
|
||||||
|
- `GET /api/admin/workspaces/{workspace_id}/logs/{log_id}`
|
||||||
```json
|
- `GET /api/admin/workspaces/{workspace_id}/usage`
|
||||||
{
|
- `GET /api/admin/workspaces/{workspace_id}/usage/operations/{operation_id}`
|
||||||
"operation_id": "op_01",
|
- `GET /api/admin/workspaces/{workspace_id}/usage/agents/{agent_id}`
|
||||||
"version": 4,
|
|
||||||
"status": "draft"
|
## 6. Page-to-endpoint mapping
|
||||||
}
|
|
||||||
```
|
### Operations catalog
|
||||||
|
|
||||||
## 5. Публикация
|
Нужны:
|
||||||
|
|
||||||
### `POST /api/admin/operations/{operation_id}/publish`
|
- список операций;
|
||||||
|
- удаление операции;
|
||||||
Назначение:
|
- edit/open operation;
|
||||||
|
- publish/archive;
|
||||||
- опубликовать текущую draft-версию.
|
- usage summary для карточек и фильтров.
|
||||||
|
|
||||||
Тело:
|
### Wizard
|
||||||
|
|
||||||
```json
|
Нужны:
|
||||||
{
|
|
||||||
"version": 4
|
- create/update version;
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Ответ:
|
|
||||||
|
|
||||||
```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;
|
|
||||||
- test run;
|
- 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. Назначение проекта
|
## 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` - внутреннее уникальное имя.
|
- глобальной сущности `Operation`;
|
||||||
- `display_name` - имя, отображаемое в UI.
|
- registry версий операций;
|
||||||
- `protocol` - `rest`, `graphql` или `grpc`.
|
- runtime adapters `REST / GraphQL / unary gRPC`;
|
||||||
- `target` - хост и протокол-специфичное описание назначения.
|
- `admin-api` для CRUD и тестовых вызовов;
|
||||||
- `input_schema` - нормализованный входной контракт.
|
- `mcp-server`, который публикует tools из published operations.
|
||||||
- `input_mapping` - правила отображения MCP-входа в поля целевого запроса.
|
|
||||||
- `execution_config` - auth-профиль, таймауты, заголовки и протокол-специфичные параметры.
|
|
||||||
- `output_mapping` - правила отображения ответа внешней системы в нормализованный выход.
|
|
||||||
- `tool_description` - метаданные для MCP и LLM.
|
|
||||||
- `status` - draft, testing, published, archived.
|
|
||||||
|
|
||||||
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 для создания и редактирования операций.
|
`Operation` остается фундаментом интеграции, но tools больше не публикуются глобально. MCP публикует tools в контексте конкретного `workspace` и конкретного `agent`.
|
||||||
- Динамический реестр операций.
|
|
||||||
- Runtime-выполнение REST операций.
|
|
||||||
- Runtime-выполнение GraphQL операций.
|
|
||||||
- Runtime-выполнение unary gRPC методов.
|
|
||||||
- Загрузка примеров `JSON` для ускоренного создания схем и mappings.
|
|
||||||
- Импорт и экспорт конфигураций в `YAML`.
|
|
||||||
- Тестирование операций до публикации.
|
|
||||||
- Публикация MCP tools на основе данных из реестра.
|
|
||||||
- Hot reload опубликованных операций без изменения backend-кода.
|
|
||||||
|
|
||||||
### Не входит в 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.
|
- gRPC streaming.
|
||||||
- Полноценный импорт OpenAPI с автоматической генерацией маппинга.
|
|
||||||
- Полноценный визуальный конструктор GraphQL-запросов.
|
|
||||||
- SOAP.
|
- SOAP.
|
||||||
- Выполнение произвольного кода внутри mapping-правил.
|
- Оркестрация workflow.
|
||||||
- Оркестрация нескольких операций в виде workflow.
|
- Биллинг.
|
||||||
- Мультитенантность и биллинг.
|
- Full RBAC policy engine.
|
||||||
|
- Traffic splitting и deployment orchestration.
|
||||||
|
|
||||||
## 4. Пользовательский сценарий
|
## 6. Пользовательские сценарии
|
||||||
|
|
||||||
Сценарий работы оператора должен быть одинаковым для всех протоколов:
|
### Оператор операций
|
||||||
|
|
||||||
1. Выбрать протокол.
|
1. Выбирает workspace.
|
||||||
2. Указать целевой хост или сервер.
|
2. Создает или редактирует operation.
|
||||||
3. Выбрать или описать внешнюю операцию.
|
3. Выполняет test run.
|
||||||
4. Определить MCP-входные параметры.
|
4. Публикует operation version.
|
||||||
5. Сопоставить MCP-вход с внешним запросом.
|
5. Привязывает operation к одному или нескольким agents.
|
||||||
6. Сопоставить внешний ответ с MCP-выходом.
|
|
||||||
7. Добавить описание для MCP и LLM.
|
|
||||||
8. Выполнить тестовый вызов.
|
|
||||||
9. Опубликовать операцию.
|
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
REST-адаптер является базовым и должен реализовываться первым.
|
|
||||||
|
|
||||||
Поддержка в MVP:
|
|
||||||
|
|
||||||
- `GET`
|
- `GET`
|
||||||
- `POST`
|
- `POST`
|
||||||
- `PUT`
|
- `PUT`
|
||||||
@@ -84,22 +167,9 @@ REST-адаптер является базовым и должен реализ
|
|||||||
- headers
|
- headers
|
||||||
- JSON request body
|
- JSON request body
|
||||||
- JSON response body
|
- JSON response body
|
||||||
- аутентификация `Bearer`, `Basic` и API key
|
|
||||||
|
|
||||||
Пользователь настраивает:
|
|
||||||
|
|
||||||
- base URL,
|
|
||||||
- HTTP method,
|
|
||||||
- path template,
|
|
||||||
- request mapping,
|
|
||||||
- response mapping.
|
|
||||||
|
|
||||||
### GraphQL
|
### GraphQL
|
||||||
|
|
||||||
Поддержка GraphQL в MVP должна быть намеренно упрощена.
|
|
||||||
|
|
||||||
Поддержка в MVP:
|
|
||||||
|
|
||||||
- `query`
|
- `query`
|
||||||
- `mutation`
|
- `mutation`
|
||||||
- endpoint URL
|
- endpoint URL
|
||||||
@@ -108,56 +178,16 @@ REST-адаптер является базовым и должен реализ
|
|||||||
- variables mapping
|
- variables mapping
|
||||||
- извлечение результата из `data`
|
- извлечение результата из `data`
|
||||||
|
|
||||||
Пользователь настраивает:
|
GraphQL в MCP публикуется как фиксированная операция с предсказуемой структурой ответа.
|
||||||
|
|
||||||
- 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.
|
|
||||||
|
|
||||||
### gRPC
|
### gRPC
|
||||||
|
|
||||||
gRPC - наиболее сложный протокол в этом проекте, поэтому его нужно ограничить на раннем этапе.
|
- только unary RPC;
|
||||||
|
- `.proto` и `descriptor set`;
|
||||||
|
- JSON-oriented schema model поверх protobuf;
|
||||||
|
- без streaming.
|
||||||
|
|
||||||
Поддержка в MVP:
|
## 8. Работа с файлами и автогенерация черновика
|
||||||
|
|
||||||
- только 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.
|
|
||||||
|
|
||||||
Поддерживаемые источники:
|
Поддерживаемые источники:
|
||||||
|
|
||||||
@@ -168,368 +198,62 @@ Streaming gRPC сознательно не входит в рамки проек
|
|||||||
|
|
||||||
Ожидаемый сценарий:
|
Ожидаемый сценарий:
|
||||||
|
|
||||||
1. Оператор загружает пример входных данных и пример ответа.
|
1. оператор загружает артефакты;
|
||||||
2. Система строит черновую схему входа и выхода.
|
2. система строит черновую схему и mapping;
|
||||||
3. Система предлагает стартовый mapping по совпадающим или близким по структуре полям.
|
3. оператор вручную корректирует результат;
|
||||||
4. Оператор вручную корректирует результат.
|
4. готовую конфигурацию можно экспортировать в `YAML`.
|
||||||
5. Для точечной настройки используется `JSONPath`.
|
|
||||||
6. Готовую конфигурацию можно экспортировать в `YAML` или импортировать обратно.
|
|
||||||
|
|
||||||
Для gRPC источником структуры является не пример JSON-сообщения, а `.proto` или descriptor set. Однако после преобразования protobuf-схемы во внутреннюю JSON-ориентированную модель пользовательский опыт должен оставаться тем же: видим структуру полей, получаем стартовый mapping, затем уточняем его вручную.
|
## 9. Внутренняя модель данных
|
||||||
|
|
||||||
`YAML` используется как человекочитаемое представление конфигурации operation для:
|
Базовые сущности:
|
||||||
|
|
||||||
- переноса между окружениями;
|
- `Workspace`
|
||||||
- резервного копирования;
|
- `Operation`
|
||||||
- хранения в git;
|
- `OperationVersion`
|
||||||
- редактирования вне UI;
|
- `Agent`
|
||||||
- пакетного импорта нескольких operation.
|
- `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`
|
На каждый вызов tool сохраняются:
|
||||||
- `name`
|
|
||||||
- `display_name`
|
- `workspace_id`
|
||||||
- `protocol`
|
- `agent_id`
|
||||||
|
- `operation_id`
|
||||||
|
- `request_id`
|
||||||
|
- `timestamp`
|
||||||
- `status`
|
- `status`
|
||||||
- `target`
|
- `duration_ms`
|
||||||
- `input_schema`
|
- `error_kind`
|
||||||
- `output_schema`
|
- `request_preview`
|
||||||
- `input_mapping`
|
- `response_preview`
|
||||||
- `output_mapping`
|
|
||||||
- `execution_config`
|
|
||||||
- `tool_description`
|
|
||||||
- `created_at`
|
|
||||||
- `updated_at`
|
|
||||||
|
|
||||||
### Target
|
Сверху строятся:
|
||||||
|
|
||||||
REST target:
|
- logs page;
|
||||||
|
- usage page;
|
||||||
|
- периодические rollups;
|
||||||
|
- latency and error aggregates.
|
||||||
|
|
||||||
- `base_url`
|
## 12. Модель маппинга
|
||||||
- `method`
|
|
||||||
- `path_template`
|
|
||||||
|
|
||||||
GraphQL target:
|
Платформе нужен отдельный слой маппинга:
|
||||||
|
|
||||||
- `endpoint`
|
- сопоставление поле-в-поле по `JSONPath`;
|
||||||
- `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`,
|
|
||||||
- константы,
|
|
||||||
- значения по умолчанию,
|
|
||||||
- извлечение вложенных полей из ответа.
|
- извлечение вложенных полей из ответа.
|
||||||
|
|
||||||
Примеры:
|
|
||||||
|
|
||||||
- `$.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. Назначение документа
|
## 1. Назначение документа
|
||||||
|
|
||||||
Этот документ фиксирует формальную модель данных платформы. Его цель - определить такие структуры, которые можно без существенных изменений перенести в:
|
Этот документ фиксирует целевую формальную модель данных платформы. Его цель - определить такие структуры, которые можно без существенных изменений перенести в:
|
||||||
|
|
||||||
- Rust domain types,
|
- Rust domain types;
|
||||||
- HTTP DTO,
|
- HTTP DTO;
|
||||||
- структуру таблиц БД,
|
- структуру таблиц БД;
|
||||||
- runtime-представление operation,
|
- runtime-представление операций и агентов;
|
||||||
- UI-формы и конфигурационные экраны.
|
- UI-формы и конфигурационные экраны.
|
||||||
|
|
||||||
Документ не привязан к конкретной СУБД, но задает каноническую JSON-модель сущностей.
|
|
||||||
|
|
||||||
## 2. Общие принципы модели
|
## 2. Общие принципы модели
|
||||||
|
|
||||||
### 2.1. Одна операция - один tool
|
### 2.1. Одна операция - один интеграционный контракт
|
||||||
|
|
||||||
Каждая `Operation` соответствует одному MCP tool. Это особенно важно для:
|
Каждая `Operation` соответствует одному интеграционному контракту:
|
||||||
|
|
||||||
- GraphQL, где одна operation соответствует одному конкретному `query` или `mutation`;
|
- GraphQL -> один конкретный `query` или `mutation`;
|
||||||
- gRPC, где одна operation соответствует одному unary-методу;
|
- gRPC -> один unary method;
|
||||||
- REST, где одна operation соответствует одному endpoint-сценарию.
|
- REST -> один endpoint-сценарий.
|
||||||
|
|
||||||
|
Однако MCP tool публикуется не напрямую из operation, а через `AgentOperationBinding` внутри конкретного `Agent`.
|
||||||
|
|
||||||
### 2.2. Внутренний транспортный формат - JSON
|
### 2.2. Внутренний транспортный формат - JSON
|
||||||
|
|
||||||
Независимо от внешнего протокола внутри системы данные должны быть представлены в JSON-ориентированном виде. Даже если внешний вызов работает с protobuf, runtime, mapping и UI опираются на нормализованный JSON.
|
Независимо от внешнего протокола внутри системы данные представлены в JSON-ориентированном виде.
|
||||||
|
|
||||||
### 2.3. Mapping всегда явный
|
### 2.3. Mapping всегда явный
|
||||||
|
|
||||||
Даже если система умеет строить черновой mapping по примерам данных, итоговая конфигурация mapping должна быть явно сохранена в operation. Нельзя полагаться на неявную "магию" сопоставления во время выполнения.
|
Даже если система умеет строить черновой mapping по примерам данных, итоговая конфигурация mapping сохраняется явно.
|
||||||
|
|
||||||
### 2.4. JSONPath как единый язык адресации
|
### 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` - уникальный идентификатор операции.
|
Помимо канонической JSON-модели система поддерживает импорт и экспорт конфигураций в `YAML`.
|
||||||
- `name` - стабильное внутреннее имя.
|
|
||||||
- `display_name` - отображаемое имя в UI.
|
## 3. Корневые сущности
|
||||||
- `protocol` - `rest`, `graphql`, `grpc`.
|
|
||||||
- `status` - `draft`, `testing`, `published`, `archived`.
|
### 3.1. `Workspace`
|
||||||
- `version` - версия конфигурации операции.
|
|
||||||
- `target` - описание внешней операции.
|
Поля:
|
||||||
- `input_schema` - схема MCP-входа.
|
|
||||||
- `output_schema` - схема MCP-выхода.
|
- `id`
|
||||||
- `input_mapping` - правила подготовки внешнего запроса.
|
- `slug`
|
||||||
- `output_mapping` - правила формирования MCP-ответа.
|
- `display_name`
|
||||||
- `execution_config` - auth, headers, timeout, retries и protocol-specific execution settings.
|
- `status`
|
||||||
- `tool_description` - описание tool для MCP и LLM.
|
- `settings`
|
||||||
- `samples` - загруженные образцы JSON и schema artifacts.
|
- `created_at`
|
||||||
- `generated_draft` - автоматически построенный черновик схем и mappings.
|
- `updated_at`
|
||||||
- `config_export` - опциональные метаданные экспортируемой конфигурации.
|
|
||||||
|
Назначение:
|
||||||
|
|
||||||
|
- логическая изоляция команд;
|
||||||
|
- 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`
|
- `created_at`
|
||||||
- `updated_at`
|
- `updated_at`
|
||||||
- `published_at`
|
- `published_at`
|
||||||
|
|
||||||
### Пример
|
### 3.3. `Agent`
|
||||||
|
|
||||||
```json
|
`Agent` - пользовательская MCP-поверхность, которая собирает ограниченный набор published operations.
|
||||||
{
|
|
||||||
"id": "op_01hr7w0m6p8x9z4n7s2k3q5t6u",
|
Поля:
|
||||||
"name": "crm_create_lead",
|
|
||||||
"display_name": "Create Lead",
|
- `id`
|
||||||
"protocol": "rest",
|
- `workspace_id`
|
||||||
"status": "draft",
|
- `slug`
|
||||||
"version": 3,
|
- `display_name`
|
||||||
"target": {
|
- `description`
|
||||||
"kind": "rest",
|
- `status`
|
||||||
"base_url": "https://api.example.com",
|
- `current_draft_version`
|
||||||
"method": "POST",
|
- `latest_published_version`
|
||||||
"path_template": "/v1/leads"
|
- `created_at`
|
||||||
},
|
- `updated_at`
|
||||||
"input_schema": {
|
- `published_at`
|
||||||
"type": "object",
|
|
||||||
"fields": {
|
### 3.4. `AgentVersion`
|
||||||
"name": {
|
|
||||||
"type": "string",
|
Снимок конфигурации агента.
|
||||||
"required": true
|
|
||||||
},
|
Поля:
|
||||||
"email": {
|
|
||||||
"type": "string",
|
- `agent_id`
|
||||||
"required": true
|
- `version`
|
||||||
}
|
- `status`
|
||||||
}
|
- `instructions`
|
||||||
},
|
- `tool_selection_policy`
|
||||||
"output_schema": {
|
- `bindings`
|
||||||
"type": "object",
|
- `created_at`
|
||||||
"fields": {
|
|
||||||
"id": {
|
### 3.5. `AgentOperationBinding`
|
||||||
"type": "string",
|
|
||||||
"required": true
|
Связь published operation с agent version.
|
||||||
},
|
|
||||||
"status": {
|
Поля:
|
||||||
"type": "string",
|
|
||||||
"required": true
|
- `operation_id`
|
||||||
}
|
- `operation_version`
|
||||||
}
|
- `tool_name`
|
||||||
},
|
- `tool_title`
|
||||||
"input_mapping": {
|
- `tool_description_override`
|
||||||
"rules": [
|
- `enabled`
|
||||||
{
|
|
||||||
"source": "$.mcp.name",
|
### 3.6. `AuthProfile`
|
||||||
"target": "$.request.body.name"
|
|
||||||
},
|
Используется только для доступа к внешним системам.
|
||||||
{
|
|
||||||
"source": "$.mcp.email",
|
Поля:
|
||||||
"target": "$.request.body.email"
|
|
||||||
}
|
- `id`
|
||||||
]
|
- `workspace_id`
|
||||||
},
|
- `name`
|
||||||
"output_mapping": {
|
- `kind`
|
||||||
"rules": [
|
- `config`
|
||||||
{
|
|
||||||
"source": "$.response.body.id",
|
### 3.7. `PlatformApiKey`
|
||||||
"target": "$.output.id"
|
|
||||||
},
|
Отдельная сущность для доступа к самой платформе.
|
||||||
{
|
|
||||||
"source": "$.response.body.status",
|
Поля:
|
||||||
"target": "$.output.status"
|
|
||||||
}
|
- `id`
|
||||||
]
|
- `workspace_id`
|
||||||
},
|
- `name`
|
||||||
"execution_config": {
|
- `prefix`
|
||||||
"timeout_ms": 10000,
|
- `scopes`
|
||||||
"auth_profile_ref": "auth_01hr7x8rj2d8nq8v0c4m4t1r9e"
|
- `status`
|
||||||
},
|
- `created_at`
|
||||||
"tool_description": {
|
- `last_used_at`
|
||||||
"title": "Create CRM lead",
|
|
||||||
"description": "Creates a new lead in CRM by name and email."
|
### 3.8. `InvocationLog`
|
||||||
},
|
|
||||||
"samples": {
|
Продуктовая запись о вызове tool.
|
||||||
"input_json_sample_ref": "file_01hr7xj7z4k1t0qm0cxsw6f1wx",
|
|
||||||
"output_json_sample_ref": "file_01hr7xkvt3m1p6ms3m7r2dr8wz"
|
Поля:
|
||||||
},
|
|
||||||
"generated_draft": {
|
- `id`
|
||||||
"status": "available",
|
- `workspace_id`
|
||||||
"source_types": ["input_json_sample", "output_json_sample"]
|
- `agent_id`
|
||||||
},
|
- `operation_id`
|
||||||
"config_export": {
|
- `request_id`
|
||||||
"format_version": "1",
|
- `level`
|
||||||
"export_mode": "portable"
|
- `status`
|
||||||
},
|
- `duration_ms`
|
||||||
"created_at": "2026-03-25T08:00:00Z",
|
- `error_kind`
|
||||||
"updated_at": "2026-03-25T08:10:00Z",
|
- `request_preview`
|
||||||
"published_at": null
|
- `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`
|
## 4. `Target`
|
||||||
|
|
||||||
@@ -162,20 +213,6 @@
|
|||||||
|
|
||||||
### 4.1. `RestTarget`
|
### 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`
|
- `kind`
|
||||||
- `base_url`
|
- `base_url`
|
||||||
- `method`
|
- `method`
|
||||||
@@ -184,19 +221,6 @@
|
|||||||
|
|
||||||
### 4.2. `GraphqlTarget`
|
### 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`
|
- `kind`
|
||||||
- `endpoint`
|
- `endpoint`
|
||||||
- `operation_type`
|
- `operation_type`
|
||||||
@@ -206,20 +230,6 @@
|
|||||||
|
|
||||||
### 4.3. `GrpcTarget`
|
### 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`
|
- `kind`
|
||||||
- `server_addr`
|
- `server_addr`
|
||||||
- `package`
|
- `package`
|
||||||
@@ -228,499 +238,23 @@
|
|||||||
- `descriptor_ref`
|
- `descriptor_ref`
|
||||||
- `descriptor_set_b64`
|
- `descriptor_set_b64`
|
||||||
|
|
||||||
`descriptor_ref` остается ссылкой на загруженный descriptor artifact в storage и registry.
|
|
||||||
|
|
||||||
`descriptor_set_b64` - runtime-ready snapshot descriptor set, который используется unary gRPC adapter для динамического вызова метода без генерации Rust-кода.
|
|
||||||
|
|
||||||
## 5. `Schema`
|
## 5. `Schema`
|
||||||
|
|
||||||
`Schema` - нормализованное описание входа или выхода. Это не JSON Schema в полном объеме, а внутренняя структурная модель, удобная для UI и runtime.
|
`Schema` - нормализованное описание входа или выхода.
|
||||||
|
|
||||||
### Базовая форма
|
Поддерживаются:
|
||||||
|
|
||||||
```json
|
- скалярные поля;
|
||||||
{
|
- вложенные объекты;
|
||||||
"type": "object",
|
- массивы;
|
||||||
"description": "Lead input",
|
- enum;
|
||||||
"fields": {
|
- nullable-поля;
|
||||||
"name": {
|
- `oneof` для protobuf.
|
||||||
"type": "string",
|
|
||||||
"required": true,
|
|
||||||
"description": "Lead full name"
|
|
||||||
},
|
|
||||||
"tags": {
|
|
||||||
"type": "array",
|
|
||||||
"required": false,
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Поддерживаемые типы
|
## 6. Принцип совместимости
|
||||||
|
|
||||||
- `object`
|
Если UI требует сущность, которой нет в текущем backend, эта сущность должна быть сначала явно добавлена в эту модель данных, а уже потом в код и БД.
|
||||||
- `array`
|
|
||||||
- `string`
|
|
||||||
- `integer`
|
|
||||||
- `number`
|
|
||||||
- `boolean`
|
|
||||||
- `enum`
|
|
||||||
- `null`
|
|
||||||
- `oneof`
|
|
||||||
|
|
||||||
### Модель поля
|
Для `Operations` и `Wizard` дополнительный уровень контрактной детализации закреплен в:
|
||||||
|
|
||||||
```json
|
- `docs/operations-workspace-contracts.md`
|
||||||
{
|
|
||||||
"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`.
|
|
||||||
|
|
||||||
Следующим логическим шагом после этого документа должна стать схема БД, в которой эти сущности будут разложены по таблицам и связям.
|
|
||||||
|
|||||||
+259
-335
@@ -2,364 +2,288 @@
|
|||||||
|
|
||||||
## 1. Назначение документа
|
## 1. Назначение документа
|
||||||
|
|
||||||
Этот документ фиксирует структуру хранения конфигураций, версий операций, загруженных артефактов и published runtime-view. Его цель - дать основу для SQL-миграций и для реализации `crank-registry`.
|
Этот документ фиксирует целевую структуру хранения workspace-scoped конфигураций, агентов, ключей доступа и observability-данных. Базовая СУБД - `PostgreSQL`.
|
||||||
|
|
||||||
В документе предполагается реляционная модель, ориентированная на `PostgreSQL`. Канонической считается схема, совместимая с `PostgreSQL`.
|
|
||||||
|
|
||||||
## 2. Общие принципы хранения
|
## 2. Общие принципы хранения
|
||||||
|
|
||||||
### 2.1. Версионирование обязательно
|
### 2.1. Версионирование обязательно
|
||||||
|
|
||||||
Конфигурация operation не должна храниться только в одной "живой" записи. Каждое существенное изменение должно приводить к появлению новой версии конфигурации.
|
Конфигурация operation и agent не хранится только в одной "живой" записи. Каждое существенное изменение создает новую версию.
|
||||||
|
|
||||||
Для MVP в registry version snapshot хранит protocol-specific конфигурацию, схемы, mapping и execution settings. Поля identity и listing view (`name`, `display_name`, `protocol`) считаются стабильными и хранятся в `operations`.
|
|
||||||
|
|
||||||
### 2.2. Published и draft разделяются логически
|
### 2.2. Published и draft разделяются логически
|
||||||
|
|
||||||
- `draft` может меняться;
|
- `draft` может меняться;
|
||||||
- `published` должна ссылаться на конкретную зафиксированную версию;
|
- `published` всегда указывает на конкретную version;
|
||||||
- runtime читает только опубликованные версии.
|
- 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. Тестовая изоляция
|
- upstream secrets живут за `secret_ref`;
|
||||||
|
- platform API keys хранятся как hash.
|
||||||
Integration tests для registry должны выполняться на реальной `PostgreSQL`, но без влияния на runtime-данные. Предпочтительный способ:
|
|
||||||
|
|
||||||
- отдельная test database;
|
|
||||||
- либо отдельная временная schema на время теста;
|
|
||||||
- обязательная очистка после завершения тестов.
|
|
||||||
|
|
||||||
## 3. Основные таблицы
|
## 3. Основные таблицы
|
||||||
|
|
||||||
Минимальный набор таблиц:
|
- `workspaces`
|
||||||
|
- `users`
|
||||||
|
- `memberships`
|
||||||
|
- `invitation_tokens`
|
||||||
- `operations`
|
- `operations`
|
||||||
- `operation_versions`
|
- `operation_versions`
|
||||||
- `published_operations`
|
- `published_operations`
|
||||||
- `operation_samples`
|
- `operation_samples`
|
||||||
- `descriptors`
|
- `descriptors`
|
||||||
- `auth_profiles`
|
- `auth_profiles`
|
||||||
|
- `agents`
|
||||||
|
- `agent_versions`
|
||||||
|
- `agent_operation_bindings`
|
||||||
|
- `published_agents`
|
||||||
|
- `platform_api_keys`
|
||||||
|
- `invocation_logs`
|
||||||
|
- `usage_rollups`
|
||||||
- `yaml_import_jobs`
|
- `yaml_import_jobs`
|
||||||
|
|
||||||
Опционально позже:
|
## 4. Operations
|
||||||
|
|
||||||
- `operation_test_runs`
|
### `operations`
|
||||||
- `audit_log`
|
|
||||||
|
- `id`
|
||||||
## 4. Таблица `operations`
|
- `workspace_id`
|
||||||
|
- `name`
|
||||||
Хранит стабильную сущность операции, не зависящую от конкретной версии.
|
- `display_name`
|
||||||
|
- `protocol`
|
||||||
### Поля
|
- `status`
|
||||||
|
- `current_draft_version`
|
||||||
- `id` `text primary key`
|
- `latest_published_version`
|
||||||
- `name` `text not null unique`
|
- `created_at`
|
||||||
- `display_name` `text not null`
|
- `updated_at`
|
||||||
- `protocol` `text not null`
|
- `published_at`
|
||||||
- `status` `text not null`
|
|
||||||
- `current_draft_version` `integer not null default 1`
|
Ограничение:
|
||||||
- `latest_published_version` `integer null`
|
|
||||||
- `created_at` `timestamptz not null`
|
- `unique (workspace_id, name)`
|
||||||
- `updated_at` `timestamptz not null`
|
|
||||||
- `published_at` `timestamptz null`
|
### `operation_versions`
|
||||||
|
|
||||||
### Назначение
|
- `operation_id`
|
||||||
|
- `version`
|
||||||
- быстрый список операций;
|
- `status`
|
||||||
- стабильный идентификатор для UI и MCP;
|
- `target_json`
|
||||||
- привязка к актуальному draft и опубликованной версии.
|
- `input_schema_json`
|
||||||
|
- `output_schema_json`
|
||||||
## 5. Таблица `operation_versions`
|
- `input_mapping_json`
|
||||||
|
- `output_mapping_json`
|
||||||
Хранит полную сериализованную конфигурацию конкретной версии operation.
|
- `execution_config_json`
|
||||||
|
- `tool_description_json`
|
||||||
### Поля
|
- `samples_json`
|
||||||
|
- `generated_draft_json`
|
||||||
- `operation_id` `text not null`
|
- `config_export_json`
|
||||||
- `version` `integer not null`
|
- `change_note`
|
||||||
- `status` `text not null`
|
- `created_at`
|
||||||
- `target_json` `jsonb not null`
|
- `created_by`
|
||||||
- `input_schema_json` `jsonb not null`
|
|
||||||
- `output_schema_json` `jsonb not null`
|
### `published_operations`
|
||||||
- `input_mapping_json` `jsonb not null`
|
|
||||||
- `output_mapping_json` `jsonb not null`
|
- `operation_id`
|
||||||
- `execution_config_json` `jsonb not null`
|
- `version`
|
||||||
- `tool_description_json` `jsonb not null`
|
- `published_at`
|
||||||
- `samples_json` `jsonb null`
|
- `published_by`
|
||||||
- `generated_draft_json` `jsonb null`
|
|
||||||
- `config_export_json` `jsonb null`
|
## 5. Operation artifacts
|
||||||
- `change_note` `text null`
|
|
||||||
- `created_at` `timestamptz not null`
|
### `operation_samples`
|
||||||
- `created_by` `text null`
|
|
||||||
|
- `id`
|
||||||
### Ключи
|
- `operation_id`
|
||||||
|
- `version`
|
||||||
- primary key: `(operation_id, version)`
|
- `sample_kind`
|
||||||
- foreign key: `operation_id -> operations(id)`
|
- `storage_ref`
|
||||||
- рекомендованный composite foreign key для связанных таблиц: `(operation_id, version)`
|
- `content_type`
|
||||||
|
- `file_name`
|
||||||
### Почему так
|
- `created_at`
|
||||||
|
|
||||||
Для MVP выгоднее хранить version snapshot целиком, а не дробить по десятку связанных таблиц. Это:
|
### `descriptors`
|
||||||
|
|
||||||
- упрощает versioning;
|
- `id`
|
||||||
- упрощает откат;
|
- `operation_id`
|
||||||
- упрощает YAML export;
|
- `version`
|
||||||
- хорошо сочетается с JSON-oriented доменной моделью.
|
- `descriptor_kind`
|
||||||
|
- `storage_ref`
|
||||||
## 6. Таблица `published_operations`
|
- `source_name`
|
||||||
|
- `package_index_json`
|
||||||
Хранит явную published-привязку, которую читает runtime.
|
- `created_at`
|
||||||
|
|
||||||
### Поля
|
### `yaml_import_jobs`
|
||||||
|
|
||||||
- `operation_id` `text primary key`
|
- `id`
|
||||||
- `version` `integer not null`
|
- `source_sample_id`
|
||||||
- `published_at` `timestamptz not null`
|
- `status`
|
||||||
- `published_by` `text null`
|
- `format_version`
|
||||||
|
- `mode`
|
||||||
### Назначение
|
- `result_operation_id`
|
||||||
|
- `result_version`
|
||||||
- быстрый доступ к published runtime-view;
|
- `error_text`
|
||||||
- отсутствие двусмысленности, какая именно версия сейчас активна;
|
- `created_at`
|
||||||
- простой invalidation для runtime cache.
|
- `finished_at`
|
||||||
|
|
||||||
### Рекомендуемая целостность
|
## 6. Upstream auth
|
||||||
|
|
||||||
- `operation_id -> operations(id)`
|
### `auth_profiles`
|
||||||
- `(operation_id, version) -> operation_versions(operation_id, version)`
|
|
||||||
|
- `id`
|
||||||
## 7. Таблица `operation_samples`
|
- `workspace_id`
|
||||||
|
- `name`
|
||||||
Хранит метаданные и ссылки на sample artifacts.
|
- `kind`
|
||||||
|
- `config_json`
|
||||||
### Поля
|
- `created_at`
|
||||||
|
- `updated_at`
|
||||||
- `id` `text primary key`
|
|
||||||
- `operation_id` `text not null`
|
Ограничение:
|
||||||
- `version` `integer not null`
|
|
||||||
- `sample_kind` `text not null`
|
- `unique (workspace_id, name)`
|
||||||
- `storage_ref` `text not null`
|
|
||||||
- `content_type` `text not null`
|
## 7. Workspaces and access layer
|
||||||
- `file_name` `text null`
|
|
||||||
- `created_at` `timestamptz not null`
|
### `workspaces`
|
||||||
|
|
||||||
### Варианты `sample_kind`
|
- `id`
|
||||||
|
- `slug`
|
||||||
- `input_json`
|
- `display_name`
|
||||||
- `output_json`
|
- `status`
|
||||||
- `yaml_import_source`
|
- `settings_json`
|
||||||
|
- `created_at`
|
||||||
### Назначение
|
- `updated_at`
|
||||||
|
|
||||||
- не класть большие sample payload в основные version records;
|
### `users`
|
||||||
- иметь возможность переиспользовать или пересобирать draft mapping;
|
|
||||||
- отслеживать, из каких sample-данных строился черновик.
|
- `id`
|
||||||
|
- `email`
|
||||||
### Рекомендуемая целостность
|
- `display_name`
|
||||||
|
- `status`
|
||||||
- `operation_id -> operations(id)`
|
- `created_at`
|
||||||
- `(operation_id, version) -> operation_versions(operation_id, version)`
|
|
||||||
|
### `memberships`
|
||||||
## 8. Таблица `descriptors`
|
|
||||||
|
- `workspace_id`
|
||||||
Хранит gRPC schema artifacts.
|
- `user_id`
|
||||||
|
- `role`
|
||||||
### Поля
|
- `created_at`
|
||||||
|
|
||||||
- `id` `text primary key`
|
### `invitation_tokens`
|
||||||
- `operation_id` `text null`
|
|
||||||
- `version` `integer null`
|
- `id`
|
||||||
- `descriptor_kind` `text not null`
|
- `workspace_id`
|
||||||
- `storage_ref` `text not null`
|
- `email`
|
||||||
- `source_name` `text null`
|
- `role`
|
||||||
- `package_index_json` `jsonb null`
|
- `status`
|
||||||
- `created_at` `timestamptz not null`
|
- `token_hash`
|
||||||
|
- `expires_at`
|
||||||
### Варианты `descriptor_kind`
|
- `created_at`
|
||||||
|
|
||||||
- `proto_upload`
|
## 8. Agents
|
||||||
- `descriptor_set`
|
|
||||||
- `reflection_snapshot`
|
### `agents`
|
||||||
|
|
||||||
### Назначение
|
- `id`
|
||||||
|
- `workspace_id`
|
||||||
- связывать gRPC operation с конкретной схемой;
|
- `slug`
|
||||||
- не хранить binary descriptor внутри основной operation version;
|
- `display_name`
|
||||||
- иметь отдельную точку для discovery metadata.
|
- `description`
|
||||||
|
- `status`
|
||||||
### Рекомендуемая целостность
|
- `current_draft_version`
|
||||||
|
- `latest_published_version`
|
||||||
- если descriptor привязан к version, то `(operation_id, version) -> operation_versions(operation_id, version)`
|
- `created_at`
|
||||||
|
- `updated_at`
|
||||||
## 9. Таблица `yaml_import_jobs`
|
- `published_at`
|
||||||
|
|
||||||
Для MVP можно импортировать YAML синхронно, но таблицу под журнал импорта лучше предусмотреть сразу.
|
Ограничение:
|
||||||
|
|
||||||
### Поля
|
- `unique (workspace_id, slug)`
|
||||||
|
|
||||||
- `id` `text primary key`
|
### `agent_versions`
|
||||||
- `source_sample_id` `text null`
|
|
||||||
- `status` `text not null`
|
- `agent_id`
|
||||||
- `format_version` `text not null`
|
- `version`
|
||||||
- `mode` `text not null`
|
- `status`
|
||||||
- `result_operation_id` `text null`
|
- `instructions_json`
|
||||||
- `result_version` `integer null`
|
- `tool_selection_policy_json`
|
||||||
- `error_text` `text null`
|
- `created_at`
|
||||||
- `created_at` `timestamptz not null`
|
|
||||||
- `finished_at` `timestamptz null`
|
### `agent_operation_bindings`
|
||||||
|
|
||||||
### Назначение
|
- `agent_id`
|
||||||
|
- `agent_version`
|
||||||
- аудит импортов;
|
- `operation_id`
|
||||||
- разбор ошибок валидации;
|
- `operation_version`
|
||||||
- поддержка будущего async import pipeline.
|
- `tool_name`
|
||||||
|
- `tool_title`
|
||||||
## 10. Таблица `auth_profiles`
|
- `tool_description_override`
|
||||||
|
- `enabled`
|
||||||
Хранит переиспользуемые профили аутентификации для внешних вызовов.
|
|
||||||
|
### `published_agents`
|
||||||
### Поля
|
|
||||||
|
- `agent_id`
|
||||||
- `id` `text primary key`
|
- `version`
|
||||||
- `name` `text not null unique`
|
- `published_at`
|
||||||
- `kind` `text not null`
|
- `published_by`
|
||||||
- `config_json` `jsonb not null`
|
|
||||||
- `created_at` `timestamptz not null`
|
## 9. Platform access and observability
|
||||||
- `updated_at` `timestamptz not null`
|
|
||||||
|
### `platform_api_keys`
|
||||||
### Варианты `kind`
|
|
||||||
|
- `id`
|
||||||
- `bearer`
|
- `workspace_id`
|
||||||
- `basic`
|
- `name`
|
||||||
- `api_key_header`
|
- `prefix`
|
||||||
- `api_key_query`
|
- `secret_hash`
|
||||||
|
- `scopes_json`
|
||||||
### Правило
|
- `status`
|
||||||
|
- `created_at`
|
||||||
`config_json` должен содержать только `secret_ref`, а не открытые секреты.
|
- `last_used_at`
|
||||||
|
|
||||||
## 11. Предлагаемая SQL-форма
|
### `invocation_logs`
|
||||||
|
|
||||||
```sql
|
- `id`
|
||||||
create table operations (
|
- `workspace_id`
|
||||||
id text primary key,
|
- `agent_id`
|
||||||
name text not null unique,
|
- `operation_id`
|
||||||
display_name text not null,
|
- `request_id`
|
||||||
protocol text not null,
|
- `level`
|
||||||
status text not null,
|
- `status`
|
||||||
current_draft_version integer not null default 1,
|
- `duration_ms`
|
||||||
latest_published_version integer null,
|
- `error_kind`
|
||||||
created_at timestamptz not null,
|
- `request_preview_json`
|
||||||
updated_at timestamptz not null,
|
- `response_preview_json`
|
||||||
published_at timestamptz null
|
- `created_at`
|
||||||
);
|
|
||||||
|
### `usage_rollups`
|
||||||
create table operation_versions (
|
|
||||||
operation_id text not null references operations(id),
|
- `workspace_id`
|
||||||
version integer not null,
|
- `agent_id`
|
||||||
status text not null,
|
- `operation_id`
|
||||||
target_json jsonb not null,
|
- `period_kind`
|
||||||
input_schema_json jsonb not null,
|
- `period_start`
|
||||||
output_schema_json jsonb not null,
|
- `calls_total`
|
||||||
input_mapping_json jsonb not null,
|
- `calls_ok`
|
||||||
output_mapping_json jsonb not null,
|
- `calls_error`
|
||||||
execution_config_json jsonb not null,
|
- `p50_ms`
|
||||||
tool_description_json jsonb not null,
|
- `p95_ms`
|
||||||
samples_json jsonb null,
|
- `p99_ms`
|
||||||
generated_draft_json jsonb null,
|
|
||||||
config_export_json jsonb null,
|
## 10. Migration strategy
|
||||||
change_note text null,
|
|
||||||
created_at timestamptz not null,
|
Переход от текущей схемы к целевой идет так:
|
||||||
created_by text null,
|
|
||||||
primary key (operation_id, version)
|
1. добавить `workspaces` и заполнить default workspace;
|
||||||
);
|
2. добавить `workspace_id` в `operations` и `auth_profiles`;
|
||||||
|
3. добавить `agents` и `published_agents`;
|
||||||
create table published_operations (
|
4. внедрить `platform_api_keys`;
|
||||||
operation_id text primary key references operations(id),
|
5. добавить `invocation_logs` и `usage_rollups`;
|
||||||
version integer not null,
|
6. перевести MCP runtime на `published_agents`, а не на глобальный список operations.
|
||||||
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 модели домена.
|
|
||||||
|
|||||||
+70
-306
@@ -2,14 +2,11 @@
|
|||||||
|
|
||||||
## 1. Назначение документа
|
## 1. Назначение документа
|
||||||
|
|
||||||
Этот документ собирает диаграммы, которые фиксируют проект до начала разработки:
|
Этот документ собирает диаграммы целевой модели проекта:
|
||||||
|
|
||||||
- компонентную структуру;
|
- компонентную структуру;
|
||||||
- связи между доменными сущностями;
|
- связи между доменными сущностями;
|
||||||
- хранение данных в БД;
|
- хранение данных в БД.
|
||||||
- основные runtime и admin-потоки.
|
|
||||||
|
|
||||||
Диаграммы даны в формате `Mermaid`, чтобы их можно было хранить прямо в репозитории и рендерить в Markdown-compatible tooling.
|
|
||||||
|
|
||||||
## 2. Компонентная диаграмма
|
## 2. Компонентная диаграмма
|
||||||
|
|
||||||
@@ -29,6 +26,7 @@ flowchart LR
|
|||||||
GRPC[adapter-grpc]
|
GRPC[adapter-grpc]
|
||||||
DB[(PostgreSQL)]
|
DB[(PostgreSQL)]
|
||||||
STORE[(Artifact Storage)]
|
STORE[(Artifact Storage)]
|
||||||
|
OBS[(Usage and Logs)]
|
||||||
|
|
||||||
UI --> ADMIN
|
UI --> ADMIN
|
||||||
MCP --> REG
|
MCP --> REG
|
||||||
@@ -36,352 +34,118 @@ flowchart LR
|
|||||||
ADMIN --> REG
|
ADMIN --> REG
|
||||||
ADMIN --> RUN
|
ADMIN --> RUN
|
||||||
ADMIN --> PROTO
|
ADMIN --> PROTO
|
||||||
|
|
||||||
REG --> DB
|
REG --> DB
|
||||||
REG --> CORE
|
REG --> CORE
|
||||||
REG --> SCHEMA
|
REG --> SCHEMA
|
||||||
REG --> MAP
|
REG --> MAP
|
||||||
|
|
||||||
RUN --> CORE
|
RUN --> CORE
|
||||||
RUN --> SCHEMA
|
RUN --> SCHEMA
|
||||||
RUN --> MAP
|
RUN --> MAP
|
||||||
RUN --> REST
|
RUN --> REST
|
||||||
RUN --> GQL
|
RUN --> GQL
|
||||||
RUN --> GRPC
|
RUN --> GRPC
|
||||||
|
|
||||||
GRPC --> PROTO
|
GRPC --> PROTO
|
||||||
PROTO --> STORE
|
PROTO --> STORE
|
||||||
ADMIN --> STORE
|
ADMIN --> STORE
|
||||||
|
REG --> OBS
|
||||||
|
ADMIN --> OBS
|
||||||
```
|
```
|
||||||
|
|
||||||
## 3. Диаграмма зависимостей crates
|
## 3. Структурная диаграмма доменной модели
|
||||||
|
|
||||||
```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. Структурная диаграмма доменной модели
|
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
classDiagram
|
classDiagram
|
||||||
|
class Workspace {
|
||||||
|
+id
|
||||||
|
+slug
|
||||||
|
+display_name
|
||||||
|
}
|
||||||
|
|
||||||
class Operation {
|
class Operation {
|
||||||
+id
|
+id
|
||||||
|
+workspace_id
|
||||||
+name
|
+name
|
||||||
+display_name
|
+display_name
|
||||||
+protocol
|
+protocol
|
||||||
+status
|
+status
|
||||||
+version
|
|
||||||
+target
|
|
||||||
+input_schema
|
|
||||||
+output_schema
|
|
||||||
+input_mapping
|
|
||||||
+output_mapping
|
|
||||||
+execution_config
|
|
||||||
+tool_description
|
|
||||||
+samples
|
|
||||||
+generated_draft
|
|
||||||
+config_export
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class RestTarget {
|
class Agent {
|
||||||
+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 {
|
|
||||||
+id
|
+id
|
||||||
+name
|
+workspace_id
|
||||||
+kind
|
+slug
|
||||||
+config
|
+display_name
|
||||||
}
|
|
||||||
|
|
||||||
class ToolDescription {
|
|
||||||
+title
|
|
||||||
+description
|
|
||||||
+tags
|
|
||||||
+examples
|
|
||||||
}
|
|
||||||
|
|
||||||
class Samples {
|
|
||||||
+input_json_sample_ref
|
|
||||||
+output_json_sample_ref
|
|
||||||
+proto_file_ref
|
|
||||||
+descriptor_ref
|
|
||||||
}
|
|
||||||
|
|
||||||
class GeneratedDraft {
|
|
||||||
+status
|
+status
|
||||||
+source_types
|
|
||||||
+generated_at
|
|
||||||
+warnings
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Operation --> RestTarget : target
|
class AgentBinding {
|
||||||
Operation --> GraphqlTarget : target
|
+operation_id
|
||||||
Operation --> GrpcTarget : target
|
+operation_version
|
||||||
Operation --> Schema : input_schema
|
+tool_name
|
||||||
Operation --> Schema : output_schema
|
+enabled
|
||||||
Operation --> MappingSet : input_mapping
|
}
|
||||||
Operation --> MappingSet : output_mapping
|
|
||||||
Operation --> ExecutionConfig : execution_config
|
class PlatformApiKey {
|
||||||
Operation --> ToolDescription : tool_description
|
+id
|
||||||
Operation --> Samples : samples
|
+workspace_id
|
||||||
Operation --> GeneratedDraft : generated_draft
|
+name
|
||||||
ExecutionConfig --> AuthProfile : auth_profile_ref
|
+prefix
|
||||||
MappingSet --> MappingRule : contains
|
+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
|
```mermaid
|
||||||
erDiagram
|
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{ OPERATION_VERSIONS : has
|
||||||
OPERATIONS ||--o| PUBLISHED_OPERATIONS : publishes
|
OPERATIONS ||--o| PUBLISHED_OPERATIONS : publishes
|
||||||
OPERATIONS ||--o{ OPERATION_SAMPLES : owns
|
AGENTS ||--o{ AGENT_VERSIONS : has
|
||||||
OPERATIONS ||--o{ DESCRIPTORS : may_use
|
AGENTS ||--o| PUBLISHED_AGENTS : publishes
|
||||||
OPERATIONS ||--o{ YAML_IMPORT_JOBS : may_create
|
AGENT_VERSIONS ||--o{ AGENT_OPERATION_BINDINGS : contains
|
||||||
AUTH_PROFILES ||--o{ OPERATION_VERSIONS : referenced_by
|
OPERATIONS ||--o{ AGENT_OPERATION_BINDINGS : exposed_by
|
||||||
|
|
||||||
|
WORKSPACES {
|
||||||
|
text id PK
|
||||||
|
text slug
|
||||||
|
text display_name
|
||||||
|
}
|
||||||
OPERATIONS {
|
OPERATIONS {
|
||||||
text id PK
|
text id PK
|
||||||
|
text workspace_id FK
|
||||||
text name
|
text name
|
||||||
text display_name
|
text display_name
|
||||||
text protocol
|
text protocol
|
||||||
text status
|
text status
|
||||||
int current_draft_version
|
|
||||||
int latest_published_version
|
|
||||||
timestamptz created_at
|
|
||||||
timestamptz updated_at
|
|
||||||
timestamptz published_at
|
|
||||||
}
|
}
|
||||||
|
AGENTS {
|
||||||
OPERATION_VERSIONS {
|
text id PK
|
||||||
text operation_id FK
|
text workspace_id FK
|
||||||
int version
|
text slug
|
||||||
|
text display_name
|
||||||
text status
|
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. Назначение документа
|
## 1. Назначение документа
|
||||||
|
|
||||||
Этот документ фиксирует порядок реализации модулей и фич. Он нужен затем, чтобы разработка шла последовательно, а не параллельно во все стороны сразу.
|
Этот документ фиксирует порядок перехода от текущего состояния проекта к целевой модели, заданной `test-ui`.
|
||||||
|
|
||||||
Принцип:
|
Принцип:
|
||||||
|
|
||||||
- сначала фундамент;
|
- сначала перепроектирование `as is -> to be`;
|
||||||
- потом минимальный end-to-end сценарий;
|
- потом foundation под workspace/agent model;
|
||||||
- потом расширение протоколов;
|
- потом возврат к end-to-end UI сценариям;
|
||||||
|
- потом observability и access layer;
|
||||||
- потом polish и demo readiness.
|
- потом polish и demo readiness.
|
||||||
|
|
||||||
## 2. Этап 0. Scaffold проекта
|
## 2. Этап 1. Перепроектирование `As Is -> To Be`
|
||||||
|
|
||||||
Цель:
|
Цель:
|
||||||
|
|
||||||
- создать `cargo workspace`;
|
- зафиксировать новую доменную модель и page-driven backend contract.
|
||||||
- создать приложения и 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.
|
|
||||||
|
|
||||||
DoD:
|
DoD:
|
||||||
|
|
||||||
- создан `cargo workspace`;
|
- зафиксирован `as is -> to be` план;
|
||||||
- все crates и apps объявлены в workspace;
|
- page-by-page gap analysis покрывает все целевые экраны;
|
||||||
- проект собирается без бизнес-логики;
|
- разобраны все архитектурные конфликты UI vs current backend;
|
||||||
- базовые test targets запускаются;
|
- документы `architecture`, `data-model`, `database-schema`, `admin-api`, `mcp-interface` синхронизированы.
|
||||||
- сделан атомарный commit со scaffold.
|
|
||||||
|
|
||||||
## 3. Этап 1. Базовая доменная модель
|
## 3. Этап 2. Workspace foundation
|
||||||
|
|
||||||
Цель:
|
Цель:
|
||||||
|
|
||||||
- реализовать типы из `data-model`.
|
- перевести хранение и API на workspace-scoped модель.
|
||||||
|
|
||||||
Фичи:
|
|
||||||
|
|
||||||
- `Operation`
|
|
||||||
- `Target`
|
|
||||||
- `Schema`
|
|
||||||
- `MappingSet`
|
|
||||||
- `ExecutionConfig`
|
|
||||||
- `ToolDescription`
|
|
||||||
- `AuthProfile`
|
|
||||||
|
|
||||||
Параллельно:
|
|
||||||
|
|
||||||
- unit tests на доменные типы;
|
|
||||||
- базовая сериализация `JSON`/`YAML`.
|
|
||||||
|
|
||||||
Результат:
|
|
||||||
|
|
||||||
- модель данных существует как код;
|
|
||||||
- нет инфраструктурных зависимостей внутри домена.
|
|
||||||
|
|
||||||
DoD:
|
DoD:
|
||||||
|
|
||||||
- типы из `data-model` реализованы;
|
- операции и auth profiles принадлежат workspace;
|
||||||
- базовая сериализация `JSON` и `YAML` проходит тесты;
|
- registry умеет фильтровать данные по workspace;
|
||||||
- доменные `impl` не содержат инфраструктурной логики;
|
- есть default workspace migration path.
|
||||||
- unit tests на ключевые типы проходят;
|
|
||||||
- изменения зафиксированы через один или несколько `RGR + commit`.
|
|
||||||
|
|
||||||
## 4. Этап 2. Schema engine
|
## 4. Этап 3. Agent publishing foundation
|
||||||
|
|
||||||
Цель:
|
Цель:
|
||||||
|
|
||||||
- реализовать `crank-schema`.
|
- ввести `Agent` и agent-scoped MCP publishing.
|
||||||
|
|
||||||
Фичи:
|
|
||||||
|
|
||||||
- model полей и типов;
|
|
||||||
- schema validation;
|
|
||||||
- field traversal;
|
|
||||||
- нормализация JSON samples;
|
|
||||||
- protobuf -> schema bridge contracts.
|
|
||||||
|
|
||||||
Результат:
|
|
||||||
|
|
||||||
- можно описывать и валидировать вход/выход.
|
|
||||||
|
|
||||||
DoD:
|
DoD:
|
||||||
|
|
||||||
- реализована схема полей и типов;
|
- можно создать agent и привязать к нему published operations;
|
||||||
- работает schema validation;
|
- `mcp-server` выдает tools в контексте конкретного agent;
|
||||||
- JSON sample normalization покрыт тестами;
|
- один agent видит только свой curated toolset.
|
||||||
- контракты protobuf -> schema зафиксированы;
|
|
||||||
- нет смешивания schema logic с adapter logic.
|
|
||||||
|
|
||||||
## 5. Этап 3. Mapping engine
|
## 5. Этап 4. Operations and wizard integration
|
||||||
|
|
||||||
Цель:
|
Цель:
|
||||||
|
|
||||||
- реализовать `crank-mapping`.
|
- посадить operations catalog и wizard на реальные backend contracts.
|
||||||
|
|
||||||
Фичи:
|
|
||||||
|
|
||||||
- `JSONPath` parsing и validation;
|
|
||||||
- input mapping;
|
|
||||||
- output mapping;
|
|
||||||
- transforms;
|
|
||||||
- generation draft mapping из samples.
|
|
||||||
|
|
||||||
Результат:
|
|
||||||
|
|
||||||
- можно преобразовывать MCP input в request model и response в output model.
|
|
||||||
|
|
||||||
DoD:
|
DoD:
|
||||||
|
|
||||||
- `JSONPath` parsing и validation работают;
|
- каталог операций и wizard работают без `localStorage` overrides;
|
||||||
- input/output mapping проходят unit tests;
|
- operation edit/delete/publish/test выполняются через backend;
|
||||||
- generation draft mapping покрыта фикстурами;
|
- все протоколы работают в рамках одного UI flow.
|
||||||
- transforms ограничены и задокументированы;
|
|
||||||
- mapping engine не знает о конкретных protocol adapters.
|
|
||||||
|
|
||||||
## 6. Этап 4. Registry и БД
|
## 6. Этап 5. Agents UI and backend
|
||||||
|
|
||||||
Цель:
|
Цель:
|
||||||
|
|
||||||
- реализовать `crank-registry` и миграции.
|
- реализовать agent-centric слой.
|
||||||
|
|
||||||
Фичи:
|
|
||||||
|
|
||||||
- таблицы из `database-schema`;
|
|
||||||
- version snapshots;
|
|
||||||
- published operations;
|
|
||||||
- auth profiles;
|
|
||||||
- sample metadata;
|
|
||||||
- descriptor metadata;
|
|
||||||
- YAML import job log.
|
|
||||||
|
|
||||||
Результат:
|
|
||||||
|
|
||||||
- конфигурации можно хранить и версионировать.
|
|
||||||
|
|
||||||
DoD:
|
DoD:
|
||||||
|
|
||||||
- миграции создают таблицы из `database-schema`;
|
- agent CRUD работает;
|
||||||
- version snapshots работают корректно;
|
- binding operations к agent работает;
|
||||||
- publish linkage реализован;
|
- published agent появляется в MCP runtime.
|
||||||
- auth profiles и artifact metadata сохраняются;
|
|
||||||
- integration tests на registry проходят на реальной БД.
|
|
||||||
|
|
||||||
## 7. Этап 5. REST vertical slice
|
## 7. Этап 6. Platform access
|
||||||
|
|
||||||
Цель:
|
Цель:
|
||||||
|
|
||||||
- получить первый рабочий end-to-end сценарий.
|
- реализовать workspace access и platform API keys.
|
||||||
|
|
||||||
Фичи:
|
|
||||||
|
|
||||||
- `crank-adapter-rest`
|
|
||||||
- `crank-runtime` для REST
|
|
||||||
- REST test run
|
|
||||||
- создание REST operation
|
|
||||||
- publish REST operation
|
|
||||||
- вызов published REST tool из MCP слоя
|
|
||||||
|
|
||||||
Результат:
|
|
||||||
|
|
||||||
- MVP работает хотя бы для REST.
|
|
||||||
|
|
||||||
DoD:
|
DoD:
|
||||||
|
|
||||||
- REST operation можно создать, протестировать и опубликовать;
|
- UI screens `API Keys`, `Settings`, `Workspace` имеют backend-контракт;
|
||||||
- runtime исполняет REST operation end-to-end;
|
- platform API keys не смешиваются с upstream auth profiles;
|
||||||
- published REST tool вызывается через MCP слой;
|
- tenant boundary выражен в access layer.
|
||||||
- negative tests на mapping и external errors существуют;
|
|
||||||
- есть демонстрационный REST сценарий.
|
|
||||||
|
|
||||||
## 8. Этап 6. Admin API v1
|
## 8. Этап 7. Observability
|
||||||
|
|
||||||
Цель:
|
Цель:
|
||||||
|
|
||||||
- дать UI полный backend-контракт для базового сценария.
|
- реализовать логи и usage.
|
||||||
|
|
||||||
Фичи:
|
|
||||||
|
|
||||||
- CRUD operations;
|
|
||||||
- create version;
|
|
||||||
- publish;
|
|
||||||
- upload input/output JSON samples;
|
|
||||||
- generate draft;
|
|
||||||
- test run;
|
|
||||||
- auth profiles CRUD;
|
|
||||||
- YAML import/export.
|
|
||||||
|
|
||||||
Результат:
|
|
||||||
|
|
||||||
- UI может полностью управлять REST operation без ручных правок кода.
|
|
||||||
|
|
||||||
DoD:
|
DoD:
|
||||||
|
|
||||||
- доступны CRUD, versioning, publish, samples, draft generation, test runs;
|
- `Logs` page и `Usage` page работают на реальных данных;
|
||||||
- доступны auth profiles и YAML import/export;
|
- есть продуктовые endpoints, а не только application logs;
|
||||||
- API контракты соответствуют документации;
|
- rollups и detail views согласованы с UI.
|
||||||
- integration tests на ключевые endpoints проходят;
|
|
||||||
- нет скрытой бизнес-логики в handlers.
|
|
||||||
|
|
||||||
## 9. Этап 7. UI v1
|
## 9. Этап 8. Alpine UI integration
|
||||||
|
|
||||||
Цель:
|
Цель:
|
||||||
|
|
||||||
- собрать рабочую административную консоль.
|
- перенести `test-ui` в `apps/ui` и подключить его к реальному backend.
|
||||||
|
|
||||||
Фичи:
|
|
||||||
|
|
||||||
- список операций;
|
|
||||||
- мастер создания операции;
|
|
||||||
- sample upload;
|
|
||||||
- schema viewer;
|
|
||||||
- mapping editor;
|
|
||||||
- test run screen;
|
|
||||||
- publish flow;
|
|
||||||
- YAML import/export screen.
|
|
||||||
|
|
||||||
Результат:
|
|
||||||
|
|
||||||
- есть демонстрируемый пользовательский интерфейс.
|
|
||||||
|
|
||||||
DoD:
|
DoD:
|
||||||
|
|
||||||
- UI покрывает основной сценарий от создания operation до publish;
|
- `apps/ui` содержит целевой Alpine.js UI;
|
||||||
- sample upload и mapping editor работают;
|
- mock JSON больше не используется на критическом пути;
|
||||||
- YAML import/export доступен из UI;
|
- UI, backend и docs синхронизированы.
|
||||||
- нет блокирующих заглушек на критическом пути демо;
|
|
||||||
- основные пользовательские сценарии проверены вручную или integration tests.
|
|
||||||
|
|
||||||
## 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:
|
DoD:
|
||||||
|
|
||||||
- `Streamable HTTP` transport работает;
|
- end-to-end demo воспроизводим;
|
||||||
- list tools и call tool реализованы;
|
- deployment и healthchecks стабильно зелёные;
|
||||||
- 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.
|
|
||||||
|
|
||||||
Такой порядок минимизирует архитектурный риск и дает ранний рабочий результат.
|
|
||||||
|
|||||||
+47
-60
@@ -2,40 +2,34 @@
|
|||||||
|
|
||||||
## 1. Назначение документа
|
## 1. Назначение документа
|
||||||
|
|
||||||
Этот документ фиксирует, как именно платформа публикует operations в виде MCP tools и какой transport используется в MVP.
|
Этот документ фиксирует, как именно платформа публикует agents и operations в виде MCP tools и какой transport используется в целевой модели.
|
||||||
|
|
||||||
Главная цель - убрать неопределенность вокруг вопроса "каким именно будет MCP server" до начала реализации.
|
|
||||||
|
|
||||||
## 2. Архитектурное решение
|
## 2. Архитектурное решение
|
||||||
|
|
||||||
Для MVP `mcp-server` должен публиковать tools через network-oriented MCP transport.
|
`mcp-server` публикует tools через network-oriented MCP transport.
|
||||||
|
|
||||||
Рекомендуемое решение:
|
Решение:
|
||||||
|
|
||||||
- основной transport: `Streamable HTTP`;
|
- основной transport: `Streamable HTTP`;
|
||||||
- отдельный `mcp-server` как сервис;
|
- отдельный `mcp-server` как сервис;
|
||||||
- `stdio` не является обязательной частью MVP.
|
- `stdio` не является обязательной частью текущего scope.
|
||||||
|
|
||||||
Причина:
|
|
||||||
|
|
||||||
- проект задуман как `Crank`, а не как локальный single-process adapter;
|
|
||||||
- нужен удаленный доступ к опубликованным tools;
|
|
||||||
- published tools должны обновляться без пересборки и без локального обертывания каждого клиента.
|
|
||||||
|
|
||||||
## 3. Модель публикации tools
|
## 3. Модель публикации tools
|
||||||
|
|
||||||
Каждая published operation превращается в один MCP tool.
|
Каждая published operation превращается в один MCP tool внутри конкретного published agent.
|
||||||
|
|
||||||
Соответствие:
|
Соответствие:
|
||||||
|
|
||||||
- одна published version;
|
- один published agent;
|
||||||
- один tool name;
|
- набор `AgentOperationBinding`;
|
||||||
|
- один tool name на binding;
|
||||||
- одна input schema;
|
- одна input schema;
|
||||||
- один результат.
|
- один результат.
|
||||||
|
|
||||||
Публикация tool основана на:
|
Публикация tool основана на:
|
||||||
|
|
||||||
- `operation.name`
|
- `agent.slug`
|
||||||
|
- `operation.name` или binding-level `tool_name`
|
||||||
- `tool_description`
|
- `tool_description`
|
||||||
- `input_schema`
|
- `input_schema`
|
||||||
- `published runtime view`
|
- `published runtime view`
|
||||||
@@ -44,7 +38,7 @@
|
|||||||
|
|
||||||
`mcp-server` должен:
|
`mcp-server` должен:
|
||||||
|
|
||||||
- загрузить published operations из registry;
|
- загрузить published agents и их bindings из registry;
|
||||||
- преобразовать их в MCP tool definitions;
|
- преобразовать их в MCP tool definitions;
|
||||||
- принимать вызовы tools от MCP clients;
|
- принимать вызовы tools от MCP clients;
|
||||||
- валидировать вход;
|
- валидировать вход;
|
||||||
@@ -62,12 +56,12 @@
|
|||||||
- заниматься protobuf discovery;
|
- заниматься protobuf discovery;
|
||||||
- содержать бизнес-логику admin UI.
|
- содержать бизнес-логику admin UI.
|
||||||
|
|
||||||
## 6. Published runtime view
|
## 6. Runtime view
|
||||||
|
|
||||||
`mcp-server` должен работать не с полной admin-конфигурацией, а с runtime-ready view.
|
В runtime view остаются:
|
||||||
|
|
||||||
В published runtime view остаются:
|
|
||||||
|
|
||||||
|
- `workspace_id`
|
||||||
|
- `agent_id`
|
||||||
- `operation_id`
|
- `operation_id`
|
||||||
- `protocol`
|
- `protocol`
|
||||||
- `target`
|
- `target`
|
||||||
@@ -78,29 +72,30 @@
|
|||||||
- `execution_config`
|
- `execution_config`
|
||||||
- `tool_description`
|
- `tool_description`
|
||||||
|
|
||||||
В published runtime view не должны попадать:
|
В runtime view не попадают:
|
||||||
|
|
||||||
- raw uploaded samples;
|
- raw uploaded samples;
|
||||||
- generated draft metadata;
|
- generated draft metadata;
|
||||||
- YAML import metadata;
|
- YAML import metadata;
|
||||||
- UI-specific helper fields.
|
- UI-specific helper fields.
|
||||||
|
|
||||||
## 7. Transport для MVP
|
## 7. MCP endpoint model
|
||||||
|
|
||||||
### Поддерживается
|
Канонический endpoint:
|
||||||
|
|
||||||
- `Streamable HTTP`
|
```text
|
||||||
|
/mcp/v1/{workspace_slug}/{agent_slug}
|
||||||
|
```
|
||||||
|
|
||||||
### Не обязательно в MVP
|
Этот endpoint определяет:
|
||||||
|
|
||||||
- `stdio`
|
- tenant boundary;
|
||||||
- дополнительные transport adapters
|
- конкретный curated toolset;
|
||||||
|
- набор usage и log labels.
|
||||||
Если позже понадобится локальная интеграция, `stdio` можно добавить как отдельный transport layer поверх того же runtime.
|
|
||||||
|
|
||||||
## 8. MCP lifecycle
|
## 8. MCP lifecycle
|
||||||
|
|
||||||
MVP-контракт `mcp-server` строится вокруг JSON-RPC методов MCP:
|
Поддерживаемые JSON-RPC методы:
|
||||||
|
|
||||||
- `initialize`
|
- `initialize`
|
||||||
- `notifications/initialized`
|
- `notifications/initialized`
|
||||||
@@ -108,37 +103,32 @@ MVP-контракт `mcp-server` строится вокруг JSON-RPC мет
|
|||||||
- `tools/list`
|
- `tools/list`
|
||||||
- `tools/call`
|
- `tools/call`
|
||||||
|
|
||||||
Сессия создается на `initialize` и идентифицируется через `MCP-Session-Id`.
|
|
||||||
Согласованная версия протокола возвращается и читается через `MCP-Protocol-Version`.
|
|
||||||
|
|
||||||
Пока сессии хранятся in-memory внутри `mcp-server`, чего достаточно для MVP и demo-сценариев.
|
|
||||||
|
|
||||||
### Tool listing
|
### Tool listing
|
||||||
|
|
||||||
После `initialize` и `notifications/initialized`:
|
|
||||||
|
|
||||||
1. клиент вызывает `tools/list`;
|
1. клиент вызывает `tools/list`;
|
||||||
2. `mcp-server` перечитывает published operations по refresh policy;
|
2. `mcp-server` извлекает `workspace_slug` и `agent_slug` из path;
|
||||||
3. строит или обновляет in-memory catalog tools;
|
3. перечитывает published agent по refresh policy;
|
||||||
4. отдает список tools через MCP JSON-RPC result.
|
4. строит или обновляет in-memory catalog tools только для этого agent;
|
||||||
|
5. отдает список tools через MCP JSON-RPC result.
|
||||||
|
|
||||||
### Tool call
|
### Tool call
|
||||||
|
|
||||||
1. MCP client вызывает tool.
|
1. клиент вызывает tool;
|
||||||
2. `mcp-server` находит published runtime view.
|
2. `mcp-server` определяет `workspace` и `agent`;
|
||||||
3. Валидирует input относительно schema.
|
3. находит binding нужной operation внутри published agent;
|
||||||
4. Делегирует вызов в `crank-runtime`.
|
4. валидирует input относительно schema;
|
||||||
5. Возвращает результат.
|
5. делегирует вызов в `crank-runtime`;
|
||||||
|
6. возвращает результат.
|
||||||
|
|
||||||
## 9. Обновление tools
|
## 9. Обновление tools
|
||||||
|
|
||||||
После публикации новой версии:
|
После публикации новой operation version или agent version:
|
||||||
|
|
||||||
1. `admin-api` фиксирует published version в registry.
|
1. `admin-api` фиксирует published version в registry;
|
||||||
2. `registry` обновляет published_operations.
|
2. `registry` обновляет published operations или published agents;
|
||||||
3. `mcp-server` не требует restart и не опирается на ручной reload signal.
|
3. `mcp-server` не требует restart;
|
||||||
4. `mcp-server` выполняет controlled refresh опубликованного каталога по interval-based policy.
|
4. выполняется controlled refresh опубликованного каталога;
|
||||||
5. Новый tool contract становится доступен MCP clients.
|
5. новый tool contract становится доступен MCP clients.
|
||||||
|
|
||||||
## 10. Именование tools
|
## 10. Именование tools
|
||||||
|
|
||||||
@@ -150,9 +140,9 @@ MVP-контракт `mcp-server` строится вокруг JSON-RPC мет
|
|||||||
|
|
||||||
Требования:
|
Требования:
|
||||||
|
|
||||||
- имя уникально в пределах платформы;
|
- имя уникально в пределах одного agent;
|
||||||
- имя не зависит от внутреннего numeric version;
|
- имя не зависит от numeric version;
|
||||||
- rename operation должен считаться отдельным осознанным изменением.
|
- один и тот же operation может публиковаться под разными именами в разных agents.
|
||||||
|
|
||||||
## 11. Ошибки MCP слоя
|
## 11. Ошибки MCP слоя
|
||||||
|
|
||||||
@@ -164,14 +154,11 @@ MVP-контракт `mcp-server` строится вокруг JSON-RPC мет
|
|||||||
- external service error;
|
- external service error;
|
||||||
- internal runtime error.
|
- internal runtime error.
|
||||||
|
|
||||||
`mcp-server` не должен терять стадию ошибки при трансляции ответа клиенту.
|
|
||||||
|
|
||||||
## 12. Практический итог
|
## 12. Практический итог
|
||||||
|
|
||||||
Для MVP достаточно следующей фиксации:
|
|
||||||
|
|
||||||
- `mcp-server` - отдельный сервис;
|
- `mcp-server` - отдельный сервис;
|
||||||
- transport - `Streamable HTTP`;
|
- transport - `Streamable HTTP`;
|
||||||
- одна published operation = один MCP tool;
|
- endpoint определяется парой `workspace + agent`;
|
||||||
|
- одна published operation = один MCP tool внутри agent;
|
||||||
- reload published tools без пересборки сервиса;
|
- reload published tools без пересборки сервиса;
|
||||||
- никакой draft-логики или admin CRUD в MCP слое.
|
- никакой draft-логики или admin CRUD в MCP слое.
|
||||||
|
|||||||
+57
-622
@@ -2,44 +2,22 @@
|
|||||||
|
|
||||||
## 1. Цель документа
|
## 1. Цель документа
|
||||||
|
|
||||||
Этот документ фиксирует детальную структуру проекта до начала активной разработки. Его задача - заранее ограничить ответственность каждого компонента, избежать разрастания `crank-core`, не допустить появления "универсальных" структур на все случаи жизни и сохранить понятные границы между доменной логикой, runtime, адаптерами, API и UI.
|
Этот документ фиксирует детальную структуру проекта под целевую модель `workspace -> agent -> operations`.
|
||||||
|
|
||||||
Основной принцип: каждый crate отвечает за один слой системы. Внутри crate модули должны быть маленькими, тематическими и с минимальным количеством публичных сущностей.
|
Основной принцип: каждый crate отвечает за один слой системы. Внутри crate модули маленькие, тематические и с минимальным количеством публичных сущностей.
|
||||||
|
|
||||||
## 2. Общие архитектурные правила
|
## 2. Общие архитектурные правила
|
||||||
|
|
||||||
### 2.1. Что считается правильной декомпозицией
|
|
||||||
|
|
||||||
- `core` содержит только базовую доменную модель, идентификаторы, типы ошибок и общие контракты.
|
- `core` содержит только базовую доменную модель, идентификаторы, типы ошибок и общие контракты.
|
||||||
- `registry` отвечает только за хранение и загрузку конфигурации операций.
|
- `registry` отвечает только за хранение и загрузку workspace-scoped конфигурации.
|
||||||
- `runtime` исполняет операции, но не знает о способе их хранения.
|
- `runtime` исполняет операции, но не знает о способе их хранения.
|
||||||
- адаптеры знают только свой протокол и общий контракт runtime.
|
- адаптеры знают только свой протокол и общий контракт runtime.
|
||||||
- `admin-api` оркестрирует use case для UI, но не содержит протокольной логики.
|
- `admin-api` оркестрирует use case для UI, но не содержит протокольной логики.
|
||||||
- `mcp-server` публикует tools и вызывает runtime, но не содержит бизнес-логики конфигурирования.
|
- `mcp-server` публикует agent-scoped tools и вызывает runtime.
|
||||||
- `ui` не знает внутреннюю реализацию runtime и работает только через HTTP API.
|
- `ui` работает только через 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 сущностей;
|
|
||||||
- отдельные модули для чтения, записи, валидации и исполнения;
|
|
||||||
- композиция из небольших сервисов вместо одного глобального сервиса.
|
|
||||||
|
|
||||||
## 3. Workspace-структура
|
## 3. Workspace-структура
|
||||||
|
|
||||||
Рекомендуемая структура:
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
crank/
|
crank/
|
||||||
apps/
|
apps/
|
||||||
@@ -58,7 +36,11 @@ crank/
|
|||||||
crank-proto/
|
crank-proto/
|
||||||
```
|
```
|
||||||
|
|
||||||
Дополнительные crates `crank-mapping`, `crank-schema` и `crank-proto` нужны затем, чтобы не перегружать `crank-core`.
|
Поверх существующих crates должны появиться новые логические поддомены:
|
||||||
|
|
||||||
|
- workspace/access domain;
|
||||||
|
- agent publishing domain;
|
||||||
|
- observability domain.
|
||||||
|
|
||||||
## 4. Детальная декомпозиция по crate
|
## 4. Детальная декомпозиция по crate
|
||||||
|
|
||||||
@@ -68,58 +50,19 @@ crank/
|
|||||||
|
|
||||||
- базовые доменные типы;
|
- базовые доменные типы;
|
||||||
- идентификаторы;
|
- идентификаторы;
|
||||||
- метаданные операций;
|
- метаданные workspace, operation и agent;
|
||||||
- общие контракты и ошибки верхнего уровня.
|
- общие контракты и ошибки.
|
||||||
|
|
||||||
Что должно лежать в crate:
|
Внутренние модули:
|
||||||
|
|
||||||
```text
|
- `ids`
|
||||||
crank-core/
|
- `protocol`
|
||||||
src/
|
- `workspace`
|
||||||
lib.rs
|
- `operation`
|
||||||
ids.rs
|
- `agent`
|
||||||
protocol.rs
|
- `auth`
|
||||||
operation/
|
- `observability`
|
||||||
mod.rs
|
- `errors`
|
||||||
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` должен быть максимально стабильным и независимым. Если положить туда все подряд, он станет точкой связности всей системы.
|
|
||||||
|
|
||||||
### 4.2. `crank-schema`
|
### 4.2. `crank-schema`
|
||||||
|
|
||||||
@@ -127,107 +70,16 @@ crank-core/
|
|||||||
|
|
||||||
- внутренняя модель схем;
|
- внутренняя модель схем;
|
||||||
- нормализация входа и выхода;
|
- нормализация входа и выхода;
|
||||||
- представление полей для UI и runtime;
|
- представление полей для 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` станет тяжелым и начнет менять версию при каждом изменении схемной логики.
|
|
||||||
|
|
||||||
### 4.3. `crank-mapping`
|
### 4.3. `crank-mapping`
|
||||||
|
|
||||||
Назначение:
|
Назначение:
|
||||||
|
|
||||||
- описание mapping DSL;
|
- mapping DSL;
|
||||||
- компиляция mappings в runtime-представление;
|
- `JSONPath` parsing;
|
||||||
- применение mappings к входу и выходу;
|
- input/output mapping;
|
||||||
- автогенерация чернового mapping по загруженным примерам;
|
- draft inference из samples.
|
||||||
- трассировка ошибок маппинга.
|
|
||||||
|
|
||||||
Структура:
|
|
||||||
|
|
||||||
```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 должен быть единым движком.
|
|
||||||
|
|
||||||
### 4.4. `crank-proto`
|
### 4.4. `crank-proto`
|
||||||
|
|
||||||
@@ -237,472 +89,55 @@ crank-mapping/
|
|||||||
- извлечение services, methods и message schemas;
|
- извлечение services, methods и message schemas;
|
||||||
- преобразование protobuf metadata во внутренние типы.
|
- преобразование 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`
|
### 4.5. `crank-registry`
|
||||||
|
|
||||||
Назначение:
|
Назначение:
|
||||||
|
|
||||||
- хранение операций, схем, descriptor links и статусов;
|
- хранение workspace-scoped operations и version snapshots;
|
||||||
- выдача draft/published представлений;
|
- хранение agents и agent versions;
|
||||||
- поиск активных операций для runtime и MCP server.
|
- auth profiles;
|
||||||
|
- platform API keys;
|
||||||
Структура:
|
- logs и usage aggregates;
|
||||||
|
- metadata по sample artifacts и descriptors.
|
||||||
```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`. Он только хранит и отдает согласованные представления.
|
|
||||||
|
|
||||||
### 4.6. `crank-runtime`
|
### 4.6. `crank-runtime`
|
||||||
|
|
||||||
Назначение:
|
Назначение:
|
||||||
|
|
||||||
- исполнение операций;
|
- исполнение published operation;
|
||||||
- orchestration между схемой, mapping и адаптерами;
|
- запись invocation events;
|
||||||
- выдача нормализованного результата.
|
- возврат нормализованного результата.
|
||||||
|
|
||||||
Структура:
|
### 4.7. Protocol adapters
|
||||||
|
|
||||||
```text
|
- `crank-adapter-rest`
|
||||||
crank-runtime/
|
- `crank-adapter-graphql`
|
||||||
src/
|
- `crank-adapter-grpc`
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
Описание:
|
Каждый adapter знает только свой протокол.
|
||||||
|
|
||||||
- `executor/operation_executor.rs` - основной orchestration use case.
|
### 4.8. `apps/admin-api`
|
||||||
- `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 представление операции.
|
|
||||||
|
|
||||||
Правило:
|
Должен содержать сервисные группы:
|
||||||
|
|
||||||
`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
|
не превращать `mcp-server` во второй `admin-api`.
|
||||||
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` останутся тонкими входными слоями;
|
|
||||||
- добавление нового протокола не потребует переписывать половину проекта.
|
|
||||||
|
|
||||||
Это и есть целевая архитектурная дисциплина проекта: отдельные слои, отдельные модели, минимально необходимая публичность и отсутствие "универсальных" структур, в которые со временем начинает стекаться вся система.
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -18,15 +18,8 @@ verify:
|
|||||||
just clippy
|
just clippy
|
||||||
just test
|
just test
|
||||||
|
|
||||||
ui-install:
|
|
||||||
cd apps/ui && npm install
|
|
||||||
|
|
||||||
ui-build:
|
ui-build:
|
||||||
cd apps/ui && npm run build
|
docker build -f apps/ui/Dockerfile .
|
||||||
|
|
||||||
ui-test:
|
|
||||||
cd apps/ui && npm run test -- --run
|
|
||||||
|
|
||||||
verify-ui:
|
verify-ui:
|
||||||
just ui-build
|
just ui-build
|
||||||
just ui-test
|
|
||||||
|
|||||||
Reference in New Issue
Block a user