feat: complete Epic 1 production foundation

This commit is contained in:
2026-08-25 01:24:11 +03:00
parent 767428436d
commit 182bde8ac0
298 changed files with 35719 additions and 5299 deletions
+138 -3
View File
@@ -16,7 +16,17 @@ Auth path:
## Авторизация
Администратор входит по email и паролю. После входа сервер устанавливает HttpOnly session cookie.
Администратор входит по email и паролю. После входа сервер устанавливает
HttpOnly session cookie и возвращает `csrf_token` для browser mutations.
Первый production-admin создаётся через локальный одноразовый bootstrap token:
```bash
crank-migrate admin-auth bootstrap-create --email owner@example.com
```
После этого оператор открывает `/login`, вводит token и задаёт первый пароль.
Token одноразовый; replay возвращает generic unauthorized без раскрытия причины.
```bash
curl -i https://crank.example.com/api/auth/login \
@@ -34,8 +44,14 @@ curl https://crank.example.com/api/auth/session \
-b 'crank_session=<cookie_value>'
```
Для небезопасных browser/API mutations с session cookie передавайте текущий
`csrf_token` в заголовке `x-csrf-token`. Cross-origin `/api/*` requests
отклоняются по умолчанию.
Endpoints:
- `GET /api/auth/bootstrap/status`
- `POST /api/auth/bootstrap/complete`
- `POST /api/auth/login`
- `POST /api/auth/logout`
- `GET /api/auth/session`
@@ -97,6 +113,8 @@ curl https://crank.example.com/api/admin/workspaces/ws_default/upstreams \
Операция описывает один REST endpoint как MCP-инструмент.
Published Version неизменяема. Изменение после публикации создаёт следующую Draft revision; существующий Agent продолжает использовать exact bound version до явного rebinding и новой публикации Agent. Архивация запрещает новые изменения/привязки, но не удаляет опубликованные snapshots или историю.
- `GET /api/admin/workspaces/{workspace_id}/operations`
- `POST /api/admin/workspaces/{workspace_id}/operations`
- `POST /api/admin/workspaces/{workspace_id}/operations/analyze-quality`
@@ -126,15 +144,26 @@ curl https://crank.example.com/api/admin/workspaces/ws_default/operations/<opera
}'
```
Успешные поля Test Run сохранены. Каждый элемент `errors` дополнительно содержит
stable `code`, `stage`, `retryability`, `outcome_certainty` и безопасные
Request/Trace IDs ответа. При `outcome_unknown` автоматический повтор запрещён;
пользователь должен сверить результат во внешней системе.
Пример публикации:
```bash
# Сначала прочитайте актуальный strong ETag из GET /operations/<operation_id>.
curl https://crank.example.com/api/admin/workspaces/ws_default/operations/<operation_id>/publish \
-b 'crank_session=<cookie_value>' \
-H 'If-Match: "<opaque-operation-etag>"' \
-H 'Content-Type: application/json' \
--data '{ "version": 1 }'
```
Mutation contract revision 2 требует `If-Match` для `PATCH`, create-version, publish, archive, delete и изменяющего существующую Operation YAML upsert. Отсутствующий token возвращает `428 operation_precondition_required`, устаревший или относящийся к другому объекту — `409 operation_stale_version`. Token повторно проверяется под тем же PostgreSQL row lock, что и mutation. Summary возвращает `can_delete`, а detail — aggregate `availability` отдельно от exact Draft/Published version state. Exact version reads имеют отдельный content-stable ETag.
Portable export использует закрытый `format_version: "2"` contract из [`schemas/operation-export-v2.schema.json`](schemas/operation-export-v2.schema.json). Он исключает persistence IDs, lifecycle metadata, samples, wizard state и credentials. Legacy v1 принимается только при импорте и нормализуется в v2; exporter v1 не выдаёт. YAML import ограничен 256 KiB и возвращает только bounded codes `operation_yaml_too_large|operation_yaml_invalid|operation_yaml_unsupported` без raw parser text.
`analyze-quality` принимает payload операции и возвращает рекомендации:
```json
@@ -182,16 +211,21 @@ curl https://crank.example.com/api/admin/workspaces/ws_default/secrets \
```
После создания или ротации API возвращает только metadata. Значение секрета нельзя прочитать повторно.
Если secret используется Auth Profile, удаление отклоняется `409 secret_referenced_by_auth_profile`.
Create, rotate, delete и denied-delete пишут bounded audit event с actor,
credential ref, request id и trace id; plaintext, ciphertext и hash в event не
попадают.
## Auth profiles
- `GET /api/admin/workspaces/{workspace_id}/auth-profiles`
- `POST /api/admin/workspaces/{workspace_id}/auth-profiles`
- `GET /api/admin/workspaces/{workspace_id}/auth-profiles/{auth_profile_id}`
- `PATCH /api/admin/workspaces/{workspace_id}/auth-profiles/{auth_profile_id}`
- `DELETE /api/admin/workspaces/{workspace_id}/auth-profiles/{auth_profile_id}`
Auth profile хранит ссылки на secrets и способ применения секрета к REST-запросу.
При выполнении Operation текущая версия Secret читается перед dispatch; rotation
Secret меняет credential для следующего execution без переписывания Operation
или Auth Profile refs.
## Agents
@@ -254,6 +288,14 @@ curl https://crank.example.com/api/admin/workspaces/ws_default/agents \
Старый формат тела из одного массива привязок поддерживается и сохраняет текущую политику агента. Предварительная проверка принимает те же `bindings` и `tool_selection_policy`, а также `query` и необязательный `group_ids`.
Agent catalog lifecycle (`agent-catalog-lifecycle-v9`) защищает опубликованный каталог от in-place mutation:
- `GET /agents/{agent_id}` возвращает strong `ETag`, связанный с workspace, Agent identity, current Draft version, latest Published version, availability и `catalog_revision`;
- mutation опубликованного Agent (`PATCH`, `DELETE`, `bindings`, `publish`, `unpublish`, `archive`) требует актуальный `If-Match`; отсутствующий precondition возвращает `428 agent_precondition_required`, устаревший — `409 agent_stale_revision`;
- Published Agent Version и его bindings immutable на уровне registry/DB; изменение каталога создаёт новый Draft/current version и отдельный publish;
- binding принимает только exact Published Operation Version из того же workspace; draft, archived, missing или foreign Operation Version отклоняются до publication;
- `catalog_revision` монотонно растёт при publish/unpublish/archive и используется MCP search/call для защиты от stale search results.
## Agent API keys
- `GET /api/admin/workspaces/{workspace_id}/agents/{agent_id}/platform-api-keys`
@@ -274,11 +316,69 @@ curl https://crank.example.com/api/admin/workspaces/ws_default/agents/<agent_id>
```
Полное значение ключа доступно только в create response.
Дальше API возвращает только metadata: `id`, `name`, bounded prefix, `key_kind`,
`scopes`, `status` и timestamps. Hash и raw key никогда не возвращаются.
`key_kind = mcp_client` используется только для MCP client доступа.
`key_kind = approval` используется только для approval side-channel. Для approval
keys можно задать `allowed_origins`; значения должны быть точными
`http://`/`https://` origins без path/query/userinfo. MCP approval запрос с
чужим `Origin` отклоняется до исполнения side effect.
`revoke` немедленно прекращает доступ, включая уже существующие MCP session.
`DELETE` переводит уже revoked key в terminal metadata state `deleted`, но не
стирает provenance. Credential mutations пишут bounded audit event с actor,
credential ref, request id и trace id; raw key/hash в audit event не попадают.
Create response для `mcp_client` key дополнительно содержит ephemeral
`connection`: canonical MCP endpoint и copy-safe конфигурации для
поддерживаемых representative clients. Это единственная граница, где API
возвращает raw secret. При повторном `GET`, после обновления страницы, revoke
или delete возвращаются только metadata; потерянное значение не восстанавливается.
После неоднозначной ошибки create UI не должен автоматически повторять запрос:
оператор сначала обновляет metadata, затем осознанно создаёт или ротирует key.
## Getting Started onboarding
- `GET /api/admin/workspaces/{workspace_id}/onboarding`
- `POST /api/admin/workspaces/{workspace_id}/onboarding/events`
`GET` доступен только аутентифицированному участнику workspace и строит
ограниченную server-authoritative проекцию: `operation`, `test`,
`publish_operation`, `agent`, `key`, `mcp_connection`, `first_call`. Browser
не передаёт completed state и не может завершить domain step. Первый read
идемпотентно фиксирует server-owned eligible cohort, а terminal projection после
реального успеха идемпотентно фиксирует completion. Оба времени выдаются в UTC
RFC3339.
Ответ имеет `schema_version: 1`, opaque `revision`, `status`
(`in_progress|complete`), `eligible_since`, упорядоченные `steps`, точные
Operation/Agent/key references, canonical `mcp_endpoint` и, только после
успешного public `tools/call`, `first_call`. Каждый step содержит stable
`id`, `completed`, `status` (`pending|current|complete|regressed`),
`action_code` и `reason_code`. В `first_call` есть только безопасные
`log_id`, Agent/key/Operation/version, tool, timestamp, Request ID и Trace ID;
входной payload, raw key и upstream body в snapshot не попадают.
`POST /onboarding/events` принимает только presentation events
`started|resumed|dismissed|abandoned`, bounded `idempotency_key` и текущий
opaque `expected_revision`. Unknown fields и попытки отправить `eligible`,
`completed`, domain steps или client supplied cohort timestamp отклоняются.
Конфликт revision возвращает `409 onboarding_stale_revision` с recovery
`reload`; запрещённый event — `422 onboarding_event_not_allowed`. Повтор того
же idempotency key безопасен и не меняет server-derived progress.
## Logs и usage
- `GET /api/admin/workspaces/{workspace_id}/logs`
- `GET /api/admin/workspaces/{workspace_id}/logs/{log_id}`
- `GET /api/admin/workspaces/{workspace_id}/logs/export.csv`
- `GET /api/admin/workspaces/{workspace_id}/usage`
- `GET /api/admin/workspaces/{workspace_id}/usage/export.csv`
- `GET /api/admin/workspaces/{workspace_id}/usage/operations/{operation_id}`
- `GET /api/admin/workspaces/{workspace_id}/usage/agents/{agent_id}`
- `GET /api/admin/workspaces/{workspace_id}/approvals`
- `GET /api/admin/workspaces/{workspace_id}/approvals/{approval_id}`
Пример:
@@ -287,6 +387,41 @@ curl 'https://crank.example.com/api/admin/workspaces/ws_default/logs?limit=20' \
-b 'crank_session=<cookie_value>'
```
Logs list принимает bounded filters `period`, `created_after`,
`created_before`, `level`, `status`, `outcome_group`, `source`, `operation_id`,
`agent_id`, `search`, `limit` и opaque `cursor`. Explicit
`created_after/created_before` задают UTC RFC3339 half-open window `[start,
end)` и должны передаваться парой. Ответ имеет форму `{ "items": [...],
"next_cursor": "..." | null }`; cursor привязан к детерминированному порядку
`created_at desc, id desc` и не раскрывает host path или секреты. Log detail
возвращает безопасные preview-поля, `request_id`, `trace_id`, execution
taxonomy, точную Operation Version и связанную Operation/Agent metadata только
в рамках текущего workspace.
`logs/export.csv` применяет те же auth, scope и filters, что и list endpoint.
CSV ограничен по строкам/размеру, использует уже отредактированные previews и
экранирует spreadsheet-formula значения (`=`, `+`, `-`, `@`, tab, CR/LF в
начале cell). CSV остаётся локальным Admin response; Crank не отправляет usage
наружу.
Usage endpoints используют UTC half-open interval `[start, end)`. Они
принимают либо bounded `period`, либо explicit RFC3339
`created_after/created_before` пару. Overview возвращает workspace summary,
timeline, breakdown по Operation/Agent и outcome группы: `success`, `upstream`,
`client`, `schema`, `crank`. Эти группы позволяют отличать ошибки внешнего
upstream или пользовательского input от ошибок самого Crank; `request_id` и
`trace_id` не используются как labels или aggregation keys. `usage/export.csv`
создаётся сервером из того же scoped usage dataset, имеет bounded размер,
экранирует spreadsheet-formula значения и не зависит от текущего client-side
snapshot браузера.
Admin API approvals являются read-only operational view. Approve/deny выполняет
MCP approval side-channel с отдельным `approval` key, потому что этот ключ можно
ограничить конкретным Agent и `allowed_origins`. Admin list/get показывает
только bounded safe request summary и terminal response payload. Raw request
payload, approval key, auth headers, confirmation/control tokens и secret-like
значения не возвращаются.
## Ошибки
Admin API возвращает JSON-ошибки с человекочитаемым сообщением и контекстом, если он доступен.