Добавить дизайн Invest Harness и контракты платформы

This commit is contained in:
admin committed 2026-11-11 04:44:47 +00:00
1 parent 5d31a39aae
commit 2af6dd78ba
21 files changed
+1322 -2

No files matched your search

+4 -2
View File
@@ -1,3 +1,5 @@
# invest-harness # Invest Harness
Invest Harness — платформа инвестиционных ИИ-агентов. Архитектура и контракты системы. Платформа инвестиционных ИИ-агентов: каталог harness’ов, плагинов, скиллов и шаблонов, настройка через формы и запуск локально или удалённо.
Проект на стадии проектирования. [Дизайн системы](designDocument/design.md).
+31
View File
@@ -0,0 +1,31 @@
# API экземпляров агентов
Общие правила: [conventions.md](conventions.md). Все ресурсы scoped к tenant. Viewer читает разрешённые агенты; operator создаёт и настраивает свои, admin — разрешённые tenant-агенты.
| Метод и путь | Запрос | Ответ | Поведение |
|---|---|---|---|
| POST /v1/agents | name, templateVersionId, config, connections | 201 Agent | Создаёт revision=1 после проверки |
| GET /v1/agents | cursor, limit | 200 Page<Agent> | Только доступные |
| GET /v1/agents/{id} | — | 200 Agent с currentRevision | Проверка owner/tenant |
| POST /v1/agents/{id}/config/validate | config, templateVersionId? | 200 ValidationResult | Без сохранения |
| PUT /v1/agents/{id}/config | config, connections; If-Match | 200 AgentRevision | Полная конфигурация; 412 lost update |
| GET /v1/agents/{id}/revisions/{revision} | — | 200 AgentRevision | Неизменяемый snapshot |
| POST /v1/agents/{id}/upgrade-preview | targetTemplateVersionId | 200 UpgradePreview | Diff, requiredPermissions, ошибки |
| POST /v1/agents/{id}/upgrade | targetTemplateVersionId, config, connections; If-Match | 200 AgentRevision | Новая ревизия, старые Run не меняются |
| DELETE /v1/agents/{id} | If-Match | 202 operationId | Архивирует; активные Run требуют отдельной отмены |
Agent: id, tenantId, ownerId, name, currentRevision, archivedAt?, createdAt. AgentRevision: id, agentId, revision, templateVersionId, configSchemaDigest, config, connections, createdAt. Connections — map логического имени на connectionId, без секретов.
ValidationResult: valid:boolean, errors:[{path, code, message}], effectiveConfig. При семантически невалидной конфигурации endpoint validate возвращает 200 valid=false; endpoints сохранения возвращают 422. Ссылки на чужие подключения не раскрываются в errors.
UpgradePreview: fromVersionId, toVersionId, changes:[{path, kind}], validation, addedPermissions. Preview не разрешает новые доступы автоматически. Принятие upgrade заново проверяет rights и policy.
## Пример создания
```json
{"name":"Обзор компаний","templateVersionId":"version_company_1","config":{"lookbackDays":90,"reportLanguage":"ru","includeSources":true},"connections":{}}
```
## Подключения
GET /v1/connections возвращает доступные ConnectionSummary (id, provider, label, grantedScopes, expiresAt), не credentials. POST /v1/connections создаёт подключение через provider authorization flow или защищённую передачу ключа; конкретные provider payload задаются отдельными адаптерами и не входят в универсальную config schema. DELETE /v1/connections/{id} отзывает платформенный доступ и при возможности выполняет revoke у провайдера. Повторное использование в новом Run после отзыва запрещено.
+37
View File
@@ -0,0 +1,37 @@
# API каталога
Общие правила: [conventions.md](conventions.md). Автор публикации — publisher соответствующего tenant. Читатель видит public или разрешённые tenant/private пакеты.
| Метод и путь | Запрос | Ответ | Доступ / особые ошибки |
|---|---|---|---|
| GET /v1/packages | kind, query, cursor, limit | 200 Page<Package> | Доступные пакеты |
| GET /v1/packages/{id} | — | 200 Package | 404 для скрытого |
| POST /v1/packages | kind, name, description, visibility | 201 Package | Publisher; 409 имя занято |
| PATCH /v1/packages/{id} | description?, visibility?; If-Match | 200 Package | Publisher; нельзя менять kind |
| GET /v1/packages/{id}/versions | cursor, limit | 200 Page<PackageVersion> | Читатель |
| GET /v1/packages/{id}/versions/{version} | — | 200 PackageVersion | Читатель |
| POST /v1/packages/{id}/versions | version, manifest | 201 VersionDraft | Publisher; Idempotency-Key |
| POST /v1/packages/{id}/versions/{version}/upload | mediaType, sizeBytes, digest | 201 UploadGrant | Publisher; ограниченный путь |
| POST /v1/packages/{id}/versions/{version}/publish | artifactRef, digest | 200 PackageVersion | Publisher; 409 другая версия; 422 несовместимость |
| POST /v1/packages/{id}/versions/{version}/policy | status, reason; If-Match | 200 VersionPolicy | Admin; status deprecated/blocked/published |
| POST /v1/resolutions | templateVersionId, environment | 200 Resolution | Читатель всех зависимостей; 422 conflict |
| GET /v1/package-versions/{id}/config | — | 200 ConfigBundle | Закреплённые schema, defaults, uiSchema и digest |
| POST /v1/package-versions/{id}/download | artifactDigest | 200 DownloadGrant | Повторная проверка policy и доступа |
## Формы
Package: id, tenantId, kind (harness/plugin/skill/template), name, description, visibility (public/tenant/private), ownerId, createdAt, revision.
VersionDraft: id, packageId, version, manifest, status=draft. PackageVersion: те же идентификаторы, artifactRef, artifactDigest, schemaDigest?, publishedAt. VersionPolicy хранится отдельно: status, reason?, revision. Поле version не разрешает перезапись содержимого.
Resolution: resolutionId, templateVersionId, packages:[{packageVersionId, kind, digest, artifactRef}], configSchemaDigest, requiredCapabilities, requiredPermissions. environment: os, architecture, protocolVersion, availableCapabilities. Resolution не содержит credentials и не обещает бессрочное право скачать заблокированный пакет.
ConfigBundle: schema (JSON Schema), defaults (object), uiSchema (object|null), schemaDigest. UI schema необязательна. OCI upload использует стандартный registry API с ограниченным токеном; upload ручка для него выдаёт registry reference и scope, а не S3 URL.
## Пример resolve
```json
{"templateVersionId":"version_company_1","environment":{"os":"linux","architecture":"amd64","protocolVersion":"1","availableCapabilities":["oci-v1","cancel-v1"]}}
```
Публикация возвращает успех только после проверки артефакта. Если загрузка завершилась, а запрос publish потерялся, повтор с тем же ключом безопасен. Удаление пакета, на который ссылаются сохранённые запуски, не входит в MVP; вместо него используются deprecated/blocked.
+33
View File
@@ -0,0 +1,33 @@
# Общие правила API v1
Это проектируемые публичные контракты Invest Harness. Базовый путь /v1; JSON UTF-8; HTTPS обязателен. Внутренние API движка не являются публичным контрактом.
## Типы и ошибки
Id — непрозрачная строка 1–128 символов; Timestamp — RFC3339 UTC; Digest — sha256: и 64 hex-символа; Revision — положительное целое. Денежные суммы задаются integer minor units + currency; CPU в millicores, память и файлы в bytes, интервалы в seconds. Дата расписания содержит отдельно IANA timezone.
Ошибка: {"error":{"code":"CONFIG_INVALID","message":"Настройки не соответствуют схеме","details":[{"path":"/lookbackDays","code":"maximum"}],"requestId":"req_example"}}. Никаких секретов или stack trace в ответе.
| HTTP | Значение |
|---|---|
| 400 | Невалидный синтаксис запроса |
| 401 | Нет действительной идентичности |
| 403 | Нет прав на известную разрешённую к раскрытию операцию |
| 404 | Ресурс отсутствует или скрыт от вызывающего |
| 409 | Конфликт состояния, digest, lease или idempotency payload |
| 412 | Устаревший If-Match |
| 422 | Валидный JSON нарушает контракт, зависимости или policy |
| 429 | Квота / rate limit, Retry-After |
| 503 | Временная недоступность зависимости |
## Доступ и повторные запросы
Сессия пользователя или сервисный bearer token. Membership и ресурсный tenant проверяются на каждой операции. Публичное чтение каталога не даёт доступ к приватным зависимостям. Cookie-мутации требуют CSRF-защиты. Запросы публикации, запуска, retry и resume требуют Idempotency-Key; область ключа tenant + principal + method + path. Hash тела связывается с ключом; другое тело → 409. Результат хранится минимум 7 дней, а уникальность attemptId и schedule occurrence действует независимо от TTL ключа.
PATCH/PUT изменяемых ресурсов требуют If-Match, ответ содержит ETag. Списки: limit 1..100, непрозрачный cursor, ответ {items:[], nextCursor:null}. Cursor фиксирует сортировку createdAt,id и фильтры, не заменяет авторизацию. DELETE с асинхронной очисткой возвращает 202 и operationId.
## События и файлы
SSE endpoint принимает Last-Event-ID; event id монотонен в пределах Run. После retention слишком старый cursor возвращает 410 с указанием получить snapshot. Повторные события дедуплицируются по eventId. SSE не является источником истины о состоянии: GET Run возвращает канонический snapshot.
Binary upload выполняется по scoped grant. Он содержит uploadId, url, expiresAt, maxBytes, requiredHeaders; постоянные записи хранят artifactId, не подписанный URL. Grant не считается подтверждением загрузки: finalize проверяет размер, digest и объект. По умолчанию ошибки не повторяются автоматически, кроме явных transient ошибок с ограниченным backoff и jitter.
+37
View File
@@ -0,0 +1,37 @@
# Runner Protocol v1
Новый контракт Invest Harness для заменяемых исполнителей. Стандартный движок подключается адаптером; он не обязан понимать этот API напрямую. ExecutionSpec одинаков для удалённой и локальной реализации.
## Регистрация
Admin создаёт POST /v1/runner-pools/{poolId}/registration-grants с {expiresInSeconds:600}; ответ 201 {grant, expiresAt}. Grant одноразовый, не журналируется. Runner вызывает POST /v1/runners/register с {grant, name, protocolVersion:"1", capabilities}; ответ 201 {runnerId, poolId, credential, expiresAt}. Долгоживущая идентичность хранится в защищённом хранилище исполнителя; credential обновляется через POST /v1/runners/{id}/credentials/rotate с текущей идентичностью и отзывается через DELETE /v1/runners/{id} администратором.
Capabilities: os, architecture, executionKinds:["oci"], features:["cancel-v1"], maxConcurrency. checkpoint-v1 и input-v1 опциональны. Профиль пула задаёт доверенную изоляцию и квоты отдельно от заявления capabilities.
## Выполнение
| Метод и путь | Запрос | Ответ |
|---|---|---|
| POST /v1/runner-jobs/claim | runnerId, availableSlots, capabilitiesRevision | 200 Assignment или 204 |
| POST /v1/attempts/{id}/heartbeat | leaseEpoch, sequence, progress? | 200 {leaseUntil, cancelRequested} |
| POST /v1/attempts/{id}/started | leaseEpoch, sequence | 200 Attempt |
| POST /v1/attempts/{id}/events | leaseEpoch, events:[RunEvent] | 200 {acceptedThrough} |
| POST /v1/attempts/{id}/uploads | leaseEpoch, purpose, mediaType, sizeBytes, digest | 201 UploadGrant |
| POST /v1/attempts/{id}/complete | leaseEpoch, completionId, outcome, resultManifest?, error? | 200 Attempt |
| POST /v1/attempts/{id}/suspend | leaseEpoch, checkpointArtifactId, inputRequest | 200 Run |
Assignment: attemptId, runId, leaseEpoch, leaseUntil, executionSpec, executionToken. Execution token scoped к Attempt. Запрос claim authenticated идентичностью Runner; последующие операции — execution token. Назначение создаётся сервером атомарно; один claim выдаёт максимум один Attempt, availableSlots не разрешает превысить maxConcurrency пула.
Outcome: succeeded/failed/cancelled. Success требует ResultManifest и сохранённых объектов, даже если artifacts пуст. Error: code, message, retryableHint; решение о retry принимает платформа. Complete повторяется с тем же completionId и телом; другой payload после terminal state → 409. Поздние heartbeat, события и complete с прежним epoch → 409 LEASE_EXPIRED. События могут доставляться повторно; уникальны по attemptId + eventId.
Suspend допустим только для checkpoint-v1 + input-v1. InputRequest: id, schema, prompt, expiresAt. Платформа принимает checkpoint, переводит Run в waiting_input, отзывает lease; новый Attempt получает checkpointRef и проверенный ответ. Без checkpoint протокол не обещает сохранение памяти процесса.
## Локальный процесс
Адаптер materialize-ит immutable ExecutionSpec в /input/execution.json, входные файлы — read-only /input/files; вывод — /output. Разрешённые secrets передаются отдельным каналом и не сериализуются в spec. Entrypoint — массив аргументов, не shell-строка. События JSONL поступают через отдельный pipe или файл адаптера, stdout/stderr считаются логами. Адаптер читает /output/result.json и загружает разрешённые артефакты; ссылки вне /output и symlink traversal запрещены.
Соглашение путей относится к стандартному OCI-адаптеру. Другой Runner может реализовать иной транспорт при сохранении семантики ExecutionSpec и результатов. Локальный offline Runner не вызывает облачные claim/heartbeat, но ведёт эквивалентное локальное состояние.
## Минимальная проверка совместимости
Runner обязан показать: корректный success; failure; отказ при несовместимом spec; отмену; тайм-аут; повтор complete без дублей; отказ с просроченным lease; изоляцию файлов между задачами; корректный upload digest; отсутствие secrets в событиях. Resume проверяется только при соответствующей capability. Managed и customer-managed пулы отображаются раздельно.
+33
View File
@@ -0,0 +1,33 @@
# API запусков и расписаний
Общие правила: [conventions.md](conventions.md). Operator запускает и отменяет доступные ему агенты; viewer читает разрешённые Run. Доступ к самому агенту не заменяет проверку прав на его connections.
| Метод и путь | Запрос | Ответ | Особенности |
|---|---|---|---|
| POST /v1/runs | agentId, agentRevision, input, poolId?, limits? | 202 Run | Idempotency-Key, policy и квоты |
| GET /v1/runs | agentId?, status?, cursor, limit | 200 Page<Run> | Tenant-scoped |
| GET /v1/runs/{id} | — | 200 Run | Каноническое состояние |
| GET /v1/runs/{id}/events | Last-Event-ID | SSE RunEvent | 410 при истёкшем cursor |
| GET /v1/runs/{id}/artifacts | cursor, limit | 200 Page<Artifact> | Без долгоживущих URL |
| POST /v1/runs/{id}/artifacts/{artifactId}/download | — | 200 DownloadGrant | Повторная проверка прав |
| POST /v1/runs/{id}/cancel | reason? | 202 Run | Идемпотентная команда; terminal run → 409 |
| POST /v1/runs/{id}/retry | acknowledgedExternalEffects:boolean | 202 новый Run | parentRunId, новый ключ, проверка policy |
| POST /v1/runs/{id}/responses | responseId, inputRequestId, value | 202 Run | Только waiting_input; схема ответа и права |
| POST /v1/schedules | agentId, cron, timezone, overlapPolicy | 201 Schedule | operator; cron и timezone валидируются |
| PATCH /v1/schedules/{id} | cron?, timezone?, enabled?, overlapPolicy?; If-Match | 200 Schedule | 412 conflict |
| GET /v1/schedules | cursor, limit | 200 Page<Schedule> | Tenant-scoped |
| DELETE /v1/schedules/{id} | If-Match | 204 | Останавливает будущие, не текущие Run |
Run: id, tenantId, agentId, agentRevisionId, status, specDigest, createdAt, startedAt?, finishedAt?, attemptCount, parentRunId?, error?, resultManifest?, inputRequest?. Полный spec доступен только субъектам с run:read-config; обычный status не содержит конфигурацию.
Schedule: id, tenantId, agentId, cron, timezone, overlapPolicy (skip/queue), enabled, nextRunAt, revision. Каждое срабатывание фиксирует текущую ревизию агента; уже созданный Run не меняется при обновлении агента. Cron интерпретируется как пять полей (минута, час, день месяца, месяц, день недели); шаг менее минуты не поддерживается.
Limits: cpuMillis, memoryBytes, timeoutSeconds, outputBytes. Пользователь может запросить меньше предела policy; превышение даёт 422, а не скрытое повышение прав. Input — JSON по input schema template. PoolId не даёт доступ к чужому пулу.
## Пример
```json
{"agentId":"agent_company","agentRevision":1,"input":{"company":"Example Company"},"limits":{"cpuMillis":1000,"memoryBytes":1073741824,"timeoutSeconds":900,"outputBytes":104857600}}
```
Отмена не гарантирует отмену уже совершённого внешнего действия. Reconcile технического статуса выполняет сервис исполнения; пользовательский запрос не переписывает состояние Runner произвольно.
+29
View File
@@ -0,0 +1,29 @@
# Каталог пакетов
Каталог — отдельный Go-сервис со своей PostgreSQL БД. Он владеет метаданными harness, plugin, skill и template. OCI-образы обслуживает registry, архивы — S3. Прямой доступ других сервисов к таблицам каталога запрещён; используется API.
## Пакет и версия
Package содержит id, tenantId, kind, name, description, visibility и ownerId. Имя уникально внутри tenant и kind. PackageVersion содержит version, manifest, artifactDigest, artifactRef, schemaDigest, createdAt и publisherId. Содержимое версии неизменяемо. Отдельная запись VersionPolicy хранит status, reason и changedAt: блокировка не переписывает исходный пакет.
Harness описывает поддерживаемый runtime, entrypoint и протокол ввода/вывода. Plugin описывает расширение и совместимость с harness. Skill содержит инструкции, ресурсы и при необходимости скрипты. Template объединяет точные или ограниченные версии зависимостей, инструкции и схему конфигурации. Skill с исполняемым кодом проходит те же проверки, что plugin.
## Публикация
Автор с ролью publisher создаёт draft, получает ограниченный upload grant и загружает артефакт. Для OCI он использует стандартный registry push. Publish проверяет размер, digest, формат манифеста, ссылки внутри архива, схему конфигурации, доступ к зависимостям и отсутствие циклов. Архив не может содержать выходящие за корень пути, абсолютные пути или небезопасные symlink.
Только после проверки артефакта транзакция публикует версию. Повтор Publish с тем же Idempotency-Key возвращает прежний результат. То же имя версии с другим digest даёт 409. Объекты без опубликованной версии очищаются после 24 часов — исходное значение, настраиваемое администратором.
## Resolve
Resolver принимает templateVersionId и профиль среды. Он строит ацикличный граф зависимостей, проверяет совместимость, выбирает конкретные версии и возвращает lock со всеми digest. Диапазоны версий разрешены на входе публикации шаблона, но не в ExecutionSpec. Конфликт версий возвращается с цепочками зависимостей; молчаливый выбор несовместимого пакета запрещён.
Доступ проверяется при resolve, выдаче download grant и назначении Attempt. Публичный шаблон не должен требовать недоступную аудитории приватную зависимость. Блокировка версии запрещает новые назначения; для уже работающих Attempt администратор выбирает policy cancel или allow_finish с аудитом.
## Обновления и хранение
Обновление шаблона не изменяет существующие AgentRevision. Пользователь получает preview изменений и ошибок валидации. Новая ревизия создаётся только после успешной проверки. В MVP миграция ручная через форму; исполняемые миграции автора не запускаются в браузере.
Deprecated версия доступна закреплённым агентам, но скрыта из стандартного выбора. Удаление referenced-версии возвращает 409. Физическая очистка допускается после retention запусков, проверки ссылок и grace period. Immutable артефакты адресуются по digest; signed URL не сохраняется в постоянном lock.
API: [catalog-api.md](api/catalog-api.md). Все диаграммы: [design.md](design.md).
+38
View File
@@ -0,0 +1,38 @@
# Пользовательская конфигурация
## Контракт автора
Шаблон содержит config.schema.json, config.defaults.json и необязательную ui.schema.json. JSON Schema описывает данные, UI Schema — только представление. Собственные параметры harness и plugins располагаются в пространствах имён harness и plugins.<packageId>; схема template объединяет их без конфликтов имён.
Базовый диалект — JSON Schema Draft 2020-12. MVP renderer поддерживает object, string, number, integer, boolean, array, enum, required, additionalProperties, minimum/maximum, minLength/maxLength, minItems/maxItems и локальные $ref/$defs. oneOf и условные секции включаются только после проверки renderer и валидатора. Неизвестные UI-компоненты и неподдерживаемые keywords отвергаются при публикации, а не игнорируются. Удалённые $ref запрещены. Ограничиваются размер, вложенность и сложность схемы. На старте запрещены пользовательские regex pattern, пока не реализованы лимиты их выполнения.
## Применение значений
1. Загрузить схему и defaults, закреплённые по digest.
2. Создать конфигурацию из defaults и явных значений пользователя: objects объединяются по ключам, arrays заменяются целиком; null допустим только по схеме и не означает удаление.
3. Проверить итоговую конфигурацию одним согласованным набором правил.
4. Проверить права на подключения и ограничения организации.
5. Сохранить полный результат как новую AgentRevision с schemaDigest.
JSON Schema default — аннотация, а не команда валидатору. defaults-файл при публикации обязан соответствовать схеме после объединения с обязательными вводимыми полями; отсутствующие обязательные поля пользователь заполняет до сохранения. Сервер не доверяет клиентской валидации.
## UI
Web UI: TypeScript, React, JSON Forms; серверный валидатор — совместимая Go-библиотека Draft 2020-12, точная реализация выбирается по общему корпусу контрактных примеров. UI schema использует только встроенные контролы и layout, без JavaScript, HTML и внешних скриптов. Описания отображаются как безопасный текст.
Поля для подключения к сервису отображают selector ConnectionRef. Секретные значения не входят в JSON конфигурации, defaults, пример или URL. readOnly, writeOnly и скрытие поля в UI не заменяют server-side permission checks. Пользователь не может через config повысить лимит ресурсов, изменить tenantId или разрешить новые сетевые доступы.
## Версии и изменения
PUT конфигурации требует If-Match. При конфликте ревизии сервер возвращает 412. UI показывает различия, пользователь повторно сохраняет результат. Запуск фиксирует agentRevisionId, schemaDigest и effectiveConfig; последующие изменения агента не влияют на Run.
Новая версия шаблона сначала проверяется на копии конфигурации. При несовместимости показываются JSON Pointer путей и сообщения ошибок. Старая ревизия остаётся рабочей до явного обновления. В MVP автоматических исполняемых миграций нет.
## Источники и пример
- [JSON Schema Draft 2020-12](https://json-schema.org/draft/2020-12)
- [Аннотации и default](https://json-schema.org/understanding-json-schema/reference/annotations)
- [JSON Forms UI Schema](https://jsonforms.io/docs/uischema/)
- [Пример схемы](contracts/examples/config.schema.json)
- [Начальные значения](contracts/examples/config.defaults.json)
- [Представление формы](contracts/examples/ui.schema.json)
+44
View File
@@ -0,0 +1,44 @@
# Модель данных
Типы в этой таблице определяют семантику API. JSON Schema ниже задают машиночитаемую основу контракта; дополнительные межресурсные проверки выполняются сервисами.
| Сущность | Ключевые поля | Владелец |
|---|---|---|
| Tenant | id, name, policyRevision | Platform API |
| Membership | tenantId, userId, roles | Identity / Platform API |
| Package | id, tenantId, kind, name, visibility, ownerId | Catalog |
| PackageVersion | id, packageId, version, manifest, artifactDigest | Catalog |
| VersionPolicy | versionId, status, reason, revision | Catalog |
| Agent | id, tenantId, ownerId, name, currentRevision | Platform API |
| AgentRevision | id, agentId, revision, templateVersionId, config, connections, schemaDigest | Platform API |
| Connection | id, tenantId, provider, scopes, encryptedSecretRef | Secrets / Platform API |
| Run | id, tenantId, agentRevisionId, status, specDigest, parentRunId | Platform API |
| Attempt | id, runId, runnerId, leaseEpoch, leaseUntil, status | Dispatcher |
| RunnerPool | id, tenantId, trustPolicy, quotas | Dispatcher |
| Runner | id, poolId, capabilities, credentialStatus, lastSeenAt | Dispatcher |
| Artifact | id, runId, attemptId, objectKey, digest, sizeBytes, mediaType | Platform API |
| Schedule | id, agentId, cron, timezone, nextRunAt, overlapPolicy | Dispatcher |
| Outbox | id, aggregateId, eventType, payload, deliveredAt | Пишущий сервис |
## Инварианты
- Между БД нет внешних ключей: идентификаторы проверяются API. Каждый сервис владеет своими транзакциями и outbox.
- Run ссылается на immutable AgentRevision и ExecutionSpec; повторная проверка прав не изменяет их содержимое.
- Published PackageVersion не перезаписывается. Политика блокировки хранится отдельно.
- Попытка имеет только одного текущего владельца lease; epoch монотонно растёт при новом назначении.
- Terminal report и смена состояния сохраняются одной транзакцией вместе с событием outbox.
- Resource ID не является доказательством доступа. Tenant scope проверяется независимо.
- ConnectionRef никогда не содержит значение секрета.
- Artifact считается готовым после проверки объекта и digest, а не после выдачи upload URL.
## Схемы
- [Манифест пакета](package-manifest.schema.json) — общий envelope четырёх видов пакетов.
- [ExecutionSpec](execution-spec.schema.json) — конкретный состав запуска.
- [Событие](run-event.schema.json) — события исполнителя.
- [Пример manifest](examples/template-manifest.json).
- [Пример ExecutionSpec](examples/execution-spec.json).
Id — opaque string. Version — SemVer строка; разрешение диапазонов выполняет resolver до фиксации ExecutionSpec. Digest — SHA-256. Timestamps — RFC3339 UTC. UUID не навязывается публичным контрактом.
Package manifest с kind=template обязан указать harnessVersionId, configSchemaRef и зависимости. Harness обязан указать entrypoint и executionKind. Plugin и skill указывают совместимость supportedHarnesses. Отсутствующая optional capability означает, что возможность недоступна; поле не может быть выведено из названия пакета.
@@ -0,0 +1,5 @@
{
"lookbackDays": 90,
"reportLanguage": "ru",
"includeSources": true
}
@@ -0,0 +1,34 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"title": "Настройки обзора компаний",
"additionalProperties": false,
"required": [
"lookbackDays",
"reportLanguage",
"includeSources"
],
"properties": {
"lookbackDays": {
"type": "integer",
"title": "Период анализа в днях",
"minimum": 1,
"maximum": 365,
"default": 90
},
"reportLanguage": {
"type": "string",
"title": "Язык отчёта",
"enum": [
"ru",
"en"
],
"default": "ru"
},
"includeSources": {
"type": "boolean",
"title": "Показывать источники",
"default": true
}
}
}
@@ -0,0 +1,44 @@
{
"protocolVersion": "1",
"runId": "run_example",
"agentRevisionId": "revision_example_1",
"templateVersionId": "template_example_1",
"packages": [
{
"packageVersionId": "template_example_1",
"kind": "template",
"digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"artifactRef": "artifact_template_example"
},
{
"packageVersionId": "harness_example_1",
"kind": "harness",
"digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"artifactRef": "registry.example.invalid/invest/runtime@sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
}
],
"configSchemaDigest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
"config": {
"lookbackDays": 90,
"reportLanguage": "ru",
"includeSources": true
},
"input": {
"company": "Example Company"
},
"connections": {},
"limits": {
"cpuMillis": 1000,
"memoryBytes": 1073741824,
"timeoutSeconds": 900,
"outputBytes": 104857600
},
"requiredCapabilities": [
"oci-v1",
"cancel-v1"
],
"entrypoint": [
"/app/entrypoint",
"/input/execution.json"
]
}
@@ -0,0 +1,27 @@
{
"manifestVersion": "1",
"kind": "template",
"name": "company-review",
"version": "1.0.0",
"artifact": {
"ref": "artifact_template_example",
"digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"mediaType": "application/gzip"
},
"dependencies": [
{
"packageVersionId": "harness_example_1",
"digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
}
],
"harnessVersionId": "harness_example_1",
"configSchemaRef": "config.schema.json",
"defaultsRef": "config.defaults.json",
"uiSchemaRef": "ui.schema.json",
"requiredCapabilities": [
"oci-v1",
"cancel-v1"
],
"requiredPermissions": [],
"retrySafe": false
}
@@ -0,0 +1,17 @@
{
"type": "VerticalLayout",
"elements": [
{
"type": "Control",
"scope": "#/properties/lookbackDays"
},
{
"type": "Control",
"scope": "#/properties/reportLanguage"
},
{
"type": "Control",
"scope": "#/properties/includeSources"
}
]
}
@@ -0,0 +1,143 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "ExecutionSpec v1",
"type": "object",
"additionalProperties": false,
"required": [
"protocolVersion",
"runId",
"agentRevisionId",
"templateVersionId",
"packages",
"configSchemaDigest",
"config",
"input",
"connections",
"limits",
"requiredCapabilities",
"entrypoint"
],
"properties": {
"protocolVersion": {
"const": "1"
},
"runId": {
"type": "string",
"minLength": 1,
"maxLength": 128
},
"agentRevisionId": {
"type": "string",
"minLength": 1,
"maxLength": 128
},
"templateVersionId": {
"type": "string",
"minLength": 1,
"maxLength": 128
},
"packages": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"packageVersionId",
"kind",
"digest",
"artifactRef"
],
"properties": {
"packageVersionId": {
"type": "string",
"minLength": 1,
"maxLength": 128
},
"kind": {
"enum": [
"harness",
"plugin",
"skill",
"template"
]
},
"digest": {
"type": "string",
"pattern": "^sha256:[a-f0-9]{64}$"
},
"artifactRef": {
"type": "string",
"minLength": 1
}
}
}
},
"configSchemaDigest": {
"type": "string",
"pattern": "^sha256:[a-f0-9]{64}$"
},
"config": {
"type": "object"
},
"input": {},
"connections": {
"type": "object",
"additionalProperties": {
"type": "string",
"minLength": 1,
"maxLength": 128
}
},
"limits": {
"type": "object",
"additionalProperties": false,
"required": [
"cpuMillis",
"memoryBytes",
"timeoutSeconds",
"outputBytes"
],
"properties": {
"cpuMillis": {
"type": "integer",
"minimum": 1
},
"memoryBytes": {
"type": "integer",
"minimum": 1
},
"timeoutSeconds": {
"type": "integer",
"minimum": 1
},
"outputBytes": {
"type": "integer",
"minimum": 1
}
}
},
"requiredCapabilities": {
"type": "array",
"uniqueItems": true,
"items": {
"type": "string",
"minLength": 1
}
},
"entrypoint": {
"type": "array",
"minItems": 1,
"items": {
"type": "string",
"minLength": 1
}
},
"checkpointRef": {
"type": "string",
"minLength": 1,
"maxLength": 128
},
"resumeInput": {}
}
}
@@ -0,0 +1,188 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Invest Harness package manifest v1",
"type": "object",
"additionalProperties": false,
"required": [
"manifestVersion",
"kind",
"name",
"version",
"artifact",
"dependencies"
],
"properties": {
"manifestVersion": {
"const": "1"
},
"kind": {
"enum": [
"harness",
"plugin",
"skill",
"template"
]
},
"name": {
"type": "string",
"minLength": 1
},
"version": {
"type": "string",
"pattern": "^\\d+\\.\\d+\\.\\d+(-[0-9A-Za-z.-]+)?(\\+[0-9A-Za-z.-]+)?$"
},
"artifact": {
"type": "object",
"additionalProperties": false,
"required": [
"ref",
"digest",
"mediaType"
],
"properties": {
"ref": {
"type": "string",
"minLength": 1
},
"digest": {
"type": "string",
"pattern": "^sha256:[a-f0-9]{64}$"
},
"mediaType": {
"type": "string",
"minLength": 1
}
}
},
"dependencies": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"packageVersionId",
"digest"
],
"properties": {
"packageVersionId": {
"type": "string",
"minLength": 1,
"maxLength": 128
},
"digest": {
"type": "string",
"pattern": "^sha256:[a-f0-9]{64}$"
}
}
}
},
"harnessVersionId": {
"type": "string",
"minLength": 1,
"maxLength": 128
},
"configSchemaRef": {
"type": "string",
"minLength": 1
},
"defaultsRef": {
"type": "string",
"minLength": 1
},
"uiSchemaRef": {
"type": "string",
"minLength": 1
},
"executionKind": {
"enum": [
"oci",
"native"
]
},
"entrypoint": {
"type": "array",
"minItems": 1,
"items": {
"type": "string",
"minLength": 1
}
},
"supportedHarnesses": {
"type": "array",
"minItems": 1,
"items": {
"type": "string",
"minLength": 1,
"maxLength": 128
}
},
"requiredCapabilities": {
"type": "array",
"uniqueItems": true,
"items": {
"type": "string",
"minLength": 1
}
},
"requiredPermissions": {
"type": "array",
"uniqueItems": true,
"items": {
"type": "string",
"minLength": 1
}
},
"retrySafe": {
"type": "boolean"
}
},
"allOf": [
{
"if": {
"properties": {
"kind": {
"const": "template"
}
}
},
"then": {
"required": [
"harnessVersionId",
"configSchemaRef"
]
}
},
{
"if": {
"properties": {
"kind": {
"const": "harness"
}
}
},
"then": {
"required": [
"executionKind",
"entrypoint"
]
}
},
{
"if": {
"properties": {
"kind": {
"enum": [
"plugin",
"skill"
]
}
}
},
"then": {
"required": [
"supportedHarnesses"
]
}
}
]
}
@@ -0,0 +1,46 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "RunEvent v1",
"type": "object",
"additionalProperties": false,
"required": [
"eventId",
"attemptId",
"sequence",
"occurredAt",
"type",
"payload"
],
"properties": {
"eventId": {
"type": "string",
"minLength": 1,
"maxLength": 128
},
"attemptId": {
"type": "string",
"minLength": 1,
"maxLength": 128
},
"sequence": {
"type": "integer",
"minimum": 1
},
"occurredAt": {
"type": "string",
"format": "date-time"
},
"type": {
"enum": [
"progress",
"log",
"artifact_ready",
"input_requested",
"diagnostic"
]
},
"payload": {
"type": "object"
}
}
}
+408
View File
@@ -0,0 +1,408 @@
# Invest Harness
## Определения
| Термин | Значение |
|---|---|
| Harness | Движок и обвязка агента: цикл выполнения, управление контекстом, состоянием и вызовами инструментов |
| Runtime | Среда выполнения программы: например, Node.js или Python; определяется пакетом harness’а |
| Plugin | Программное расширение, добавляющее инструменты, интеграции или возможности |
| Skill | Инструкции и вспомогательные материалы для конкретной задачи; может содержать скрипты, но сам по себе не предоставляет доступы |
| Шаблон агента | Инструкции, совместимый harness, плагины, скиллы и схема пользовательских настроек |
| Экземпляр агента | Шаблон с настройками и подключениями пользователя |
| Run | Логический запуск с неизменяемым снимком версий, входных данных и настроек |
| Attempt | Одна попытка исполнения Run, назначенная конкретному Runner |
| Runner | Заменяемый исполнитель, подготавливающий окружение и запускающий harness |
| Каталог | Сервис метаданных пакетов, версий, совместимости и прав доступа с собственной БД |
| Registry | API доставки OCI-образов; байты образов находятся в отдельном хранилище |
| ExecutionSpec | Неизменяемое описание запуска, общее для локального и удалённого исполнения |
| Capability | Проверяемая возможность исполнителя: среда, протокол, отмена, checkpoint |
| Tenant | Организация или персональное пространство с отдельной границей доступа |
| Lease | Ограниченное по времени назначение Attempt исполнителю |
## Концепция и статус
Invest Harness — платформа агентов для инвестиций. Автор публикует шаблон и его зависимости, пользователь выбирает шаблон, настраивает его через автоматически построенную форму и запускает локально или удалённо. Оба режима используют один формат пакетов и ExecutionSpec.
Документ задаёт целевую архитектуру и контракты первой версии. Реализация сервисов пока не входит в этот репозиторий. Выбранные технологии — проектные решения; численные показатели масштабирования — гипотезы до измерений.
Для платформы содержимое окружения Runner — чёрный ящик. Внутри могут работать агентские цепочки, произвольные программы, разные модели и интеграции. Платформа отвечает за доставку пакетов, доступы, ресурсы, состояния и результаты. Она не задаёт инвестиционную стратегию и не гарантирует корректность финансовых выводов или внешних действий. Способность продолжить работу после сбоя должна быть явно объявлена пакетом.
Локальный запуск уже загруженного публичного пакета не требует облачного аккаунта. Сеть может требоваться модели или инструментам. Публичность описания пакета не означает доступ к пользовательским данным.
## Пользовательские сценарии
1. Автор загружает версию пакета и схемы, проверяет зависимости и публикует версию.
2. Пользователь выбирает шаблон, заполняет форму и подключает разрешённые интеграции. Сохраняется ревизия экземпляра агента.
3. Пользователь запускает агента локально, в управляемом пуле или на своём Runner.
4. Платформа показывает состояние, события, файлы и итог, позволяет отменить запуск.
5. Если harness поддерживает запрос ввода и checkpoint, пользователь может ответить и продолжить выполнение отдельной попыткой.
6. Обновление шаблона создаёт новую ревизию агента после проверки настроек; текущие запуски не меняются.
## Архитектура C4
Контейнер в C4 — отдельно запускаемое приложение или хранилище, не обязательно Docker-контейнер. Диаграммы используют нотацию Mermaid с явными типами элементов C4. C3 раскрывает логические модули, а не отдельные микросервисы.
### C1 Контекст
```mermaid
flowchart LR
U["Пользователь / Person"]
A["Автор и администратор / Person"]
P["Invest Harness / Software System"]
M["Модели и внешние интеграции / External System"]
L["Локальное окружение пользователя / External System"]
U -->|Выбор, настройка, запуск| P
A -->|Публикация и управление| P
P -->|Разрешённые вызовы| M
P -->|Пакеты и конфигурация| L
L -->|Вызовы по локальным доступам| M
```
### C2 Контейнеры и хранилища
```mermaid
flowchart TB
U["Пользователь / Person"]
subgraph P["Invest Harness / Software System"]
G["Gateway / Container / Nginx"]
W["Web UI / Container / React, TypeScript, JSON Forms"]
I["Identity / Logical component of platform API / Sessions, OIDC"]
B["Platform API / Container / Go, REST"]
DB[("Platform DB / PostgreSQL / роли, агенты, запуски, аудит")]
C["Каталог / Container / Go, REST"]
CD[("Catalog DB / PostgreSQL / пакеты и версии")]
PS[("Package Storage / S3 / архивы плагинов, скиллов, шаблонов")]
D["Диспетчер / Container / Go"]
E["Сервис исполнения / Container / Go / workflow, jobs, адаптер"]
ED[("Execution DB / PostgreSQL / назначения и статусы движка")]
R["Registry API / Component of execution service / Go, OCI"]
RS[("Registry Storage / S3 / манифесты и слои образов")]
ES[("Execution Storage / S3 / логи и технические артефакты")]
X["Runner pool / Containers / стандартный адаптер или свой Runner"]
J["Job / Ephemeral container / harness и зависимости"]
T["Опциональный шлюз инструментов / Container / Go"]
S["Secrets / Managed service / KMS-backed encryption"]
F[("User Storage / S3 / вход, результат, checkpoint")]
O["Наблюдаемость / OTel, Prometheus, Grafana, Loki"]
end
L["Local client / Container on user device / Go CLI, Docker"]
M["LLM и внешние API / External Systems"]
U --> G
G --> W
G --> B
W -->|Вход| I
I --> DB
B -->|Проверка личности| I
B --> DB
B --> C
C --> CD
C --> PS
D -->|Готовые задания| DB
D -->|Зафиксировать версии| C
D -->|Назначение через адаптер| E
E --> ED
E --> ES
R -->|Метаданные| ED
R -->|Байты образов| RS
X -->|Runner Protocol, lease, heartbeat| D
X -->|OCI pull через API| R
X --> J
J -->|Токен запуска| B
J --> F
J --> T
J -->|При явно разрешённом прямом доступе| M
T -->|Проверка прав и бюджета| B
T --> S
T --> M
B --> F
D --> S
B -.-> O
C -.-> O
E -.-> O
X -.-> O
T -.-> O
U --> L
L -->|Пакеты| C
L -->|Образы| R
L -->|Локальные credentials| M
```
Путь X → D обозначает публичный протокол собственной реализации Runner. Стандартный исполнитель соединяется со своим движком через адаптер внутри сервиса исполнения; он не обязан нативно поддерживать этот протокол. Диспетчер остаётся единственным владельцем бизнес-решения о назначении. Нельзя одновременно выдавать один Attempt через обе системы очередей.
Gateway логически обслуживает публичные HTTPS-маршруты; внутренние стрелки сокращены. Авторизация выполняется в принимающем сервисе и не заменяется наличием Gateway. Registry на схеме вынесен для наглядности, но может работать в одном процессе с сервисом исполнения. S3 buckets логически отдельны; физический сервис может быть общим.
### C3 Каталог
```mermaid
flowchart LR
U["Platform API / Author"]
subgraph C["Каталог / Container"]
A["REST API / Go"]
P["Проверка прав и видимости"]
V["Проверка манифеста, схем и digest"]
Q["Resolver зависимостей и совместимости"]
N["Публикация неизменяемой версии"]
end
DB[("Catalog DB / PostgreSQL")]
S[("Package Storage / S3")]
R["Registry API / OCI"]
U --> A
A --> P
P --> V
P --> Q
V --> N
V --> S
V --> R
N --> DB
Q --> DB
```
### C3 Исполнение и registry
```mermaid
flowchart TB
D["Диспетчер / Go"]
X["Стандартный исполнитель или свой Runner"]
OCI["OCI client / Docker Engine"]
subgraph E["Сервис исполнения / Container"]
A["Execution adapter API / Go"]
P["Сервисная авторизация"]
W["Workflow и jobs"]
L["Назначения и состояния движка"]
H["Приём событий, логов и завершения"]
R["Registry API / OCI push и pull"]
end
DB[("Execution DB / PostgreSQL")]
RS[("Registry Storage / S3 / image bytes")]
ES[("Execution Storage / S3 / logs and artifacts")]
D --> A
A --> P
P --> W
W --> L
X -->|Через соответствующий адаптер| L
X --> H
H --> L
L --> DB
H --> ES
OCI --> R
R --> P
R -->|Метаданные| DB
R -->|Манифесты и слои| RS
```
### Технологии и границы реализации
| Система | Стек | Как создаётся |
|---|---|---|
| Web UI | TypeScript, React, JSON Forms | Новый интерфейс |
| Platform API и identity | Go, REST, OpenAPI, сессии, OIDC | Продуктовый API с адаптацией существующих механизмов аккаунтов |
| Каталог | Go, PostgreSQL, S3 API | Новый отдельный сервис |
| Диспетчер | Go, PostgreSQL, transactional outbox | Новый слой квот и назначений |
| Сервис исполнения | Go, SQL, существующие workflow/jobs | Адаптация готового ядра; внутренние детали скрыты контрактом |
| Registry | Go, OCI Distribution API, PostgreSQL, S3 | Переиспользуемый компонент ядра |
| Стандартный Runner | Go, Docker Engine | Готовый исполнитель с адаптером |
| Собственный Runner | Любой язык, HTTPS Runner Protocol v1 | Независимая реализация; требуется проверка совместимости |
| Локальный клиент | Go CLI, Docker Engine | Общий resolver и ExecutionSpec; локальное состояние в файлах |
| Хранилища | PostgreSQL; S3-совместимое хранилище | Отдельные базы, buckets и сервисные учётные записи |
| Secrets | Envelope encryption, KMS; локально OS keychain | Провайдер выбирается при развёртывании |
| Шлюз инструментов | Go, HTTPS | Опциональный новый сервис |
| Наблюдаемость | OpenTelemetry, Prometheus, Grafana, Loki | Интеграция; без payload и секретов по умолчанию |
| Начальное развёртывание | Linux, Docker Compose, Nginx | Управление и исполнители на разных машинах |
Runtime внутри harness’а не ограничен языком платформы. Kubernetes — будущая реализация Runner, не зависимость MVP. Kafka и отдельный координатор кластера в MVP не требуются.
## Публикация пакета
```mermaid
sequenceDiagram
participant A as Автор
participant C as Каталог
participant S as Package Storage / Registry
participant D as Catalog DB
A->>C: Создать draft и запросить upload
C-->>A: Ограниченный адрес загрузки
A->>S: Загрузить архив или OCI-образ
A->>C: Publish с digest и манифестом
C->>S: Проверить наличие, размер и digest
C->>C: Проверить схему и зависимости
C->>D: Зафиксировать immutable version
C-->>A: Published version
```
Метаданные опубликованы только после проверки объекта. Незавершённые загрузки убирает сборщик мусора после grace period. Каталог хранит четыре типа пакетов: harness, plugin, skill, template. Версии неизменяемы; блокировка — изменяемая политика доступа поверх версии. Детали: [каталог](catalog.md).
## Настройка через JSON Schema
Автор шаблона задаёт config.schema.json, config.defaults.json и необязательную ui.schema.json. Схемы настроек harness’а и плагинов подключаются шаблоном в отдельные пространства имён. Пользователь меняет значения, не схему автора. Secret-поля содержат ссылки на подключения, а не значения credentials.
```mermaid
sequenceDiagram
participant U as Пользователь
participant W as Web / локальная форма
participant C as Каталог
participant B as Platform API / локальный валидатор
W->>C: Загрузить закреплённую схему и defaults
C-->>W: JSON Schema и UI Schema
W-->>U: Форма настроек
U->>W: Значения и ссылки на подключения
W->>B: Сохранить конфигурацию
B->>B: Проверить схему, права и policy
B-->>W: Новая immutable revision
```
Сервер повторно валидирует данные. Скрытое поле формы не является защитой. Defaults применяются отдельным шагом: JSON Schema default не заполняет данные автоматически. [Конфигурация и UI](configuration-ui.md).
## Локальный запуск
```mermaid
sequenceDiagram
participant U as Пользователь
participant L as Local client
participant C as Каталог / локальный кеш
participant R as Local Runner
participant H as Harness
U->>L: Запустить экземпляр агента
L->>C: Resolve и проверить digest
L->>L: Валидировать config, собрать ExecutionSpec
L->>R: Spec и локальные secret bindings
R->>H: Запустить в ограниченном окружении
H-->>R: События и результат
R-->>L: Итог и локальные файлы
```
## Удалённый запуск
```mermaid
sequenceDiagram
participant U as Пользователь
participant B as Platform API
participant C as Каталог
participant D as Диспетчер
participant R as Runner / адаптер
participant S as Хранилища
U->>B: Создать Run с Idempotency-Key
B->>C: Resolve точных версий
B->>B: Сохранить Run и outbox в одной транзакции
D->>B: Обработать готовый Run
R->>D: Claim с capabilities
D-->>R: Attempt, lease, ExecutionSpec
R->>S: Загрузить пакеты по digest
R->>D: Heartbeat и события
R->>S: Загрузить результаты
R->>D: Complete с lease epoch и manifest результатов
D->>B: Финализировать Run
B-->>U: Статус и доступ к файлам
```
Обработка допускает повторную доставку; exactly-once внешних действий не обещается. Отзыв lease блокирует поздние записи в платформу, но не может отменить уже отправленный запрос во внешний сервис. [Исполнение](execution.md).
## Авторизация
```mermaid
sequenceDiagram
participant U as Пользователь
participant I as Identity
participant B as API
participant R as Runner
participant T as Tool gateway
participant S as Secrets
U->>I: Вход
I-->>U: Сессия
U->>B: Создать запуск
B->>B: Tenant, роль, пакет, подключения, квоты
B-->>R: Токен только назначенного Attempt
R->>T: Разрешённая операция
T->>B: Проверить права и актуальность Attempt
T->>S: Получить нужный credential
T-->>R: Результат без долгоживущего ключа
```
Прямые интеграции разрешаются явно: секрет может потребоваться внутри окружения. В таком режиме runner-owner становится частью границы доверия, а стоимость внешних вызовов нельзя ограничить только платформенным счётчиком. [Доступы и изоляция](security.md).
## Состояния запуска
```mermaid
stateDiagram-v2
[*] --> queued
queued --> preparing: назначение
preparing --> running: окружение готово
running --> waiting_input: checkpoint и запрос ввода
waiting_input --> queued: подтверждённое продолжение
running --> succeeded: результат принят
preparing --> failed: ошибка подготовки
running --> failed: ошибка
preparing --> interrupted: потеря lease
running --> interrupted: потеря lease
interrupted --> queued: разрешённый повтор
interrupted --> failed: повтор запрещён
queued --> cancelled: отмена
waiting_input --> cancelled: отмена
preparing --> cancelling: запрос отмены
running --> cancelling: запрос отмены
cancelling --> cancelled: остановка подтверждена
cancelling --> interrupted: остановка не подтверждена
succeeded --> [*]
failed --> [*]
cancelled --> [*]
```
Переход в waiting_input освобождает исполнителя только после durable checkpoint. Для harness без checkpoint этот режим не предоставляется. При потере lease от собственного Runner результат внешних действий может остаться неизвестным.
## Замена исполнителей
```mermaid
flowchart TB
D["Диспетчер / tenant policy, capabilities, quotas"]
P["Runner Protocol v1 / HTTPS"]
A["Адаптер стандартного движка"]
R["Собственный Runner"]
K["Kubernetes Runner / после MVP"]
L["Local Runner"]
D --> P
P --> A
P --> R
P --> K
P --> L
```
Нативный Runner и адаптер не создают две независимые аренды одного Attempt: платформа хранит lease, адаптер сопоставляет его с executionId движка и подавляет дубликаты. Локальный автономный режим использует тот же ExecutionSpec, но не обязан регистрироваться в облаке. [Протокол Runner](api/executor-api.md).
## Масштабирование
```mermaid
flowchart LR
API["API replicas"] --> DB[("PostgreSQL / runs + outbox")]
D["Dispatcher replicas / atomic claims"] --> DB
D --> Q["Логические очереди tenants / fair scheduling"]
Q --> A["Управляемый пул"]
Q --> B["Собственные пулы tenants"]
A --> S[("S3 + локальные кеши образов")]
B --> S
A --> M["Внешние API / rate limits"]
B --> M
```
Ёмкость определяется активными запусками, длительностью и ресурсами; добавление исполнителей ограничено квотами внешних API. Реализации движка с одной активной репликой не масштабируются простым дублированием процесса: сначала проверяются ограничения ядра, затем шардирование по пулам. Формулы, отказы, SLO и retention: [масштабирование](scaling.md).
## MVP и развитие
MVP: четыре типа пакетов; immutable versions; конфигурация через формы; экземпляры агентов; ручной и регулярный запуск; стандартный и локальный Runner; документированный протокол своей реализации; отмена, события и результаты; tenant isolation; registry и buckets; аудит.
Следующий этап: возобновление через checkpoint, пользовательский ввод, автоматическая миграция конфигураций, Kubernetes Runner, автоматическое масштабирование и коммерческий биллинг. Протокол заранее резервирует эти возможности, но UI не предлагает их без подтверждённой capability.
Открытые решения: провайдер KMS и S3, конкретные версии зависимостей, политика модерации публичных пакетов, целевые SLO после нагрузочных измерений, перечень лицензий распространяемых runtime и пакетов. Публичный доступ к этому проекту сам по себе не задаёт лицензию его дальнейшего использования.
## Подробные спецификации
- [Каталог и версии](catalog.md)
- [JSON Schema Driven UI](configuration-ui.md)
- [Исполнение](execution.md)
- [Авторизация и изоляция](security.md)
- [Масштабирование](scaling.md)
- [Общие правила API](api/conventions.md)
- [API каталога](api/catalog-api.md)
- [API агентов](api/agents-api.md)
- [API запусков](api/runs-api.md)
- [Runner Protocol](api/executor-api.md)
- [Модель данных и JSON Schema](contracts/domain-model.md)
+35
View File
@@ -0,0 +1,35 @@
# Исполнение
## Граница чёрного ящика
Harness получает ExecutionSpec и разрешённые ресурсы. Его внутренний цикл не управляется платформой. Для участия в платформе пакет предоставляет entrypoint и адаптер событий. Exit code 0 означает успешное завершение процесса, но Run становится succeeded только после проверки ResultManifest и сохранения артефактов. Ошибка финансового рассуждения не определяется статусом процесса.
## Планирование
API в одной транзакции записывает Run, reservation квоты и outbox. Диспетчер обрабатывает outbox идемпотентно по runId. Resolve выполняется до создания снимка; назначение повторно проверяет block policy и права. Попытка захватывается атомарно с leaseEpoch. Стандартный execution adapter использует runId/attemptId как ключ дедупликации, хранит соответствие с executionId движка и сверяет состояние при неоднозначном ответе.
Расписания хранят IANA timezone, cron expression, nextRunAt и overlapPolicy: skip или queue. В MVP catch-up после простоя создаёт не более одного пропущенного запуска. Уникальный ключ scheduleId + scheduledAt предотвращает дубли. При переходах DST пропущенное локальное время пропускается, повторённое выполняется один раз; scheduledAt фиксируется как UTC instant. Ручной запуск имеет отдельный ключ.
## Lease и повторы
Начальные настройки: lease 90 секунд, heartbeat каждые 20 секунд, maxAttempts 3 только для явно retry-safe задач. Важна серверная шкала времени. Каждый heartbeat и terminal report содержит attemptId, leaseEpoch и sequence. Устаревший epoch возвращает 409 и не может изменить состояние Run.
Потеря lease переводит Run в interrupted. Автоповтор разрешён, только если template объявил retrySafe и policy организации позволяет; иначе нужен явный retry пользователя после предупреждения о возможном повторе внешних действий. Checkpoint continuation создаёт новый Attempt того же Run. Retry terminal Run создаёт новый Run с parentRunId и тем же снимком, но с актуальной проверкой прав.
Fencing защищает записи платформы, но не останавливает старый процесс на недоступной машине и не обеспечивает exactly-once сторонних API. Повторные внешние действия должен защищать сам harness или поддерживающий idempotency key шлюз.
## Отмена и ожидание
Cancel queued снимает reservation и сразу завершает Run. Cancel running устанавливает cancelling; Runner прекращает процесс и возвращает подтверждение. Для управляемого Runner после grace period выполняется принудительная остановка. Без подтверждения собственного Runner состояние становится interrupted, а не falsely cancelled. Поздний success после принятой отмены не отменяет запрос пользователя.
Waiting_input поддерживается только с capability checkpoint-v1: Runner сохраняет checkpoint, передаёт его digest и input schema, получает подтверждение, затем освобождает lease. Ответ пользователя валидируется, разрешение проверяется заново. Повтор responseId не создаёт ещё одну попытку. Новое продолжение закрепляется за исходным ExecutionSpec.
## Локальный режим
Клиент скачивает lock и артефакты, проверяет digest и разрешения, сохраняет ExecutionSpec, запускает локальный Runner. Credentials поступают из OS keychain или выбранного пользователем провайдера. Облачная регистрация не обязательна для кешированных публичных пакетов. Синхронизация результатов с облаком — отдельное явное действие, не поведение по умолчанию.
## Артефакты
Runner запрашивает upload grant для конкретного Attempt и назначения файла. Grant ограничен размером, сроком и префиксом. ResultManifest содержит artifactId, digest, size и mediaType, без произвольных URL. Сервис проверяет существование объектов до terminal success. Логи доставляются отдельно и не являются единственным источником состояния.
[Runner Protocol](api/executor-api.md) · [Состояния и диаграммы](design.md).
+58
View File
@@ -0,0 +1,58 @@
# Масштабирование Invest Harness
## Модель нагрузки
Число аккаунтов не определяет ёмкость. Измеряем arrival rate λ, среднюю и p95 длительность T, CPU и память Attempt, размер образа, объём логов, количество внешних запросов и число одновременных подключений. Раздельные классы: короткие интерактивные задачи, длинные фоновые, периодические и сборка пакетов.
По Little’s law среднее число активных задач L = λ × T. Для целевой загрузки слотов ρ начальная оценка slots ≥ λ × T / ρ. Пример-гипотеза: 120 запусков/мин, 180 секунд на запуск, ρ=0.7 → около 515 слотов. Это не 515 серверов: число слотов на узле ограничено одновременно CPU, RAM, диском и квотами API. Хвосты длительности требуют нагрузочного измерения, а не подстановки p95 в доказательство SLO.
## Очереди и backpressure
Admission control резервирует tenant concurrency и бюджет перед назначением. Weighted fair scheduling чередует tenants; внутри tenant приоритет + возраст. Ограничены queue depth и max queue age. При превышении пользователь получает 429 с Retry-After либо явный queued status по принятой policy. Бесконечного скрытого backlog быть не должно.
Учитываем лимиты внешних API, токены/мин, соединения и доступность источников. Шлюз контролирует свои вызовы. Для прямых вызовов Runner платформа гарантирует только ресурсные/временные лимиты, а финансовый предел зависит от провайдера credentials.
## Масштабирование компонентов
| Компонент | Стратегия | Ограничение |
|---|---|---|
| Stateless API и каталог | Несколько реплик за Gateway | Пулы SQL и согласованность прав |
| Диспетчер | Atomic claims, partitioning по tenant/pool | Единственное назначение Attempt и fencing |
| Сервис исполнения | Сначала ресурсы и отдельные пулы, затем проверенное шардирование | Нельзя предполагать поддержку active-active ядром |
| Runner | Масштабирование по ready queue age и свободным ресурсам | Внешние API и время загрузки образа |
| PostgreSQL | Индексы, bounded pagination, архивирование; read replicas для допустимо устаревших чтений | Назначения и авторизация читаются из актуального источника |
| S3 / registry | Прямые scoped grants, кеш по digest, лимит параллельных загрузок | Bandwidth, egress, cache isolation |
Горячие индексы: Run(tenantId, createdAt, id), Run(status, nextEligibleAt, id), Attempt(poolId, leaseUntil), Package(tenantId, kind, name), уникальный PackageVersion(packageId, version). Большие события и логи не хранятся целиком в строке Run.
## Прогрев и масштабирование исполнителей
Managed pool хранит небольшой warm reserve. Scale-out — по времени ожидания совместимых задач, scale-in — после drain: новые задания не выдаются, активные завершаются. Кеш неизменяемых образов по digest экономит загрузки. Приватные образы и архивы не выдаются другому tenant без проверки доступа. Garbage collection кеша использует размер и last-used; pinned active layers не удаляются.
Собственный пул имеет независимые лимиты и SLA владельца. Недоступность такого пула не переносит задание автоматически в управляемый: credentials и политика размещения могут запрещать перенос.
## Отказы
| Сбой | Поведение |
|---|---|
| API после записи Run | Повтор Idempotency-Key возвращает прежний Run |
| Dispatcher после отправки job | Reconcile по attemptId, без второго внешнего запуска |
| Runner потерял связь | Lease expires → interrupted; retry только по policy |
| S3 недоступен при завершении | Нет succeeded до проверки ResultManifest |
| Catalog недоступен | Новые resolve ждут/ошибка; закреплённые запускать только при актуальной policy |
| Registry недоступен | Кеш допустим при проверке digest и актуальных прав; иначе очередь |
| SQL недоступен | Новые назначения остановлены; terminal reports повторяются идемпотентно |
## Retention и восстановление
Исходные настраиваемые значения: orphan uploads 24 часа; debug-логи 14 дней; результаты 30 дней; аудит 180 дней. Это проектные значения, а не юридические обязательства. Активные Run и checkpoints защищены от очистки. Образы удаляются только после проверки ссылок, grace period и доступности backup.
Backup включает БД, объекты, ключевые конфигурации и возможность восстановления ключей шифрования. Проверяется согласованное восстановление metadata → digest → object. Цели для первой проверки: RPO 1 час, RTO 4 часа, без обещания до rehearsal.
## Метрики и проверка
Предлагаемые цели пилота: p95 control API <500 мс без внешних вызовов; p95 queue wait <30 секунд при нагрузке в пределах квоты и доступном пуле. Время выполнения чёрного ящика не включается в API SLO. Следим за queue age, interrupted rate, attempts/run, event lag, orphan bytes, cache hit, SQL saturation и стоимостью на Run.
Нагрузочные сценарии: равномерный поток, массовый scheduled burst с jitter, один шумный tenant, недоступный внешний API, перезапуск dispatcher, потеря Runner, медленный S3, дубли terminal reports. Отдельно проверяем отсутствие чужих артефактов и credentials.
Стоимость = вычисления + хранение + egress + управляемые вызовы моделей. Цена прямых сторонних вызовов может быть неизвестна. Биллинг не строится исключительно на недоверенных отчётах пользовательского Runner.
+31
View File
@@ -0,0 +1,31 @@
# Доступы и изоляция
## Идентичность и роли
Tenant — персональное или организационное пространство. Роли: owner управляет доступами и удалением; admin — настройками и Runner; publisher — публикацией пакетов; operator — запуском и отменой доступных агентов; viewer — чтением разрешённых ресурсов. Сервер получает tenant из проверенного membership, а не доверяет tenantId в запросе. Каждый доступ к agent, run, package, connection и artifact проверяется отдельно.
Identity в MVP — модуль Platform API с существующими механизмами аккаунтов и сессий; внешний OIDC подключается при необходимости. Web использует HttpOnly Secure SameSite cookie и CSRF-защиту мутаций. CLI использует ограниченный токен; device authorization добавляется при поддержке identity-провайдером. Подписанный токен проверяется по issuer, audience, expiry и revocation policy.
## Исполнители
Владелец tenant создаёт одноразовый registration grant для пула. Runner обменивает grant на собственную служебную идентичность. Отзыв блокирует новые задания и доступы. Capabilities — заявление исполнителя, а не доказательство изоляции; управляемые пулы проходят conformance checks. Пользовательские пулы явно маркируются как customer-managed.
Каждому Attempt выдаётся короткоживущий токен с runId, attemptId, tenantId, leaseEpoch, audience и scope. Он не предоставляет чтение чужих Run и доступ к администрированию. Сервер проверяет активность lease при чувствительных операциях.
## Секреты и интеграции
ConnectionRef указывает на запись подключения. Секрет хранится с envelope encryption, ключ расшифровки управляется KMS. Значения не попадают в config, логи, схемы, артефакты и signed URL. Доступ ограничивается интеграцией и операцией. Шлюз по возможности вызывает API сам; для прямого доступа выдаются scoped credentials или секрет монтируется только в окружение конкретной попытки.
Own Runner имеет техническую возможность прочитать выданные ему credentials; эта граница доверия показывается владельцу при назначении подключений. Отзыв токена платформы не отзывает уже раскрытый сторонний ключ: для этого нужен revoke у провайдера.
## Изоляция
Управляемое выполнение: отдельная рабочая директория и контейнер, non-root, лимиты CPU/RAM/PIDs/диска/времени, отсутствие host mounts, privileged mode и Docker socket внутри job. Сетевые политики ограничивают обращения к внутренним адресам и metadata endpoints. Для недоверенного кода выбирается усиленный sandbox runtime; один обычный контейнер не считается полной защитой от побега.
Runner Protocol не навязывает реализацию sandbox, но диспетчер не назначает защищённые задачи пулу без нужной trust policy. Невозможность обеспечить ограничение должна приводить к отказу до запуска.
## Аудит и данные
Аудит: actor, tenant, action, resource, result, timestamp, requestId. Без содержимого prompts и secrets по умолчанию. История публикаций, выдачи credentials, назначения, отмены, обновления настроек и изменений прав сохраняется отдельно от debug-логов.
Удаление tenant сначала блокирует новые запуски, отзывает доступ, останавливает задачи и удаляет данные по retention policy. Backup retention и сроки окончательного удаления явно сообщаются пользователю. Публичность пакета не распространяется на Run, connections или результаты.