diff --git a/personal/plans/balda-callee-prism-plan.md b/personal/plans/balda-callee-prism-plan.md old mode 100755 new mode 100644 index 700c7cac..5c0ad7ba --- a/personal/plans/balda-callee-prism-plan.md +++ b/personal/plans/balda-callee-prism-plan.md @@ -1,665 +1,30 @@ -> [!WARNING] Исторический документ -> Не использовать как runbook текущей Balda. Актуальная локальная схема, пути и команды: [[personal/projects/balda/setup|Balda — актуальный локальный запуск]]. -> Текущий контейнер: `balda-mm-mac`; config: `/Users/admin/Docker/balda-mm-mac/config/config.yaml`; Obsidian MCP: `type: http`, `http://host.docker.internal:3101/mcp`. -> Материал ниже сохранён только как история. -> Актуальные результаты локального прогона 2026-10-14, статус кандидата rc.26 и незавершённые планы: [[personal/projects/balda/execution-broker|Execution Broker — текущий локальный контур]]. Старые NO-GO и SHA ниже относятся к указанным историческим запускам. +# Balda / Execution Broker — план приёмки -# Текущее состояние — Balda / Broker — 2026-10-03 22:41 Бишкек +Проверено: **2026-10-15**. Общая release-ready приёмка ещё не закрыта. Точные работающие версии и доказательства: [[personal/projects/balda/execution-broker]]. Запуск двух Balda: [[personal/projects/balda/setup]]. -Статус: **приёмочный Docker-прогон выполнен на прежнем образе; автоматическая публикация исправлена, а Balda запущена из текущего исправленного HEAD PR #12 (`73e02156`). Общий release остаётся NO-GO до нового end-to-end прогона, подтверждающего публикацию и уведомления на образе из текущего HEAD `73e02156`.** Записи ниже — история и могут содержать устаревшие статусы. +## Контур -- **Прогон:** `run-fbe2cb459fe2965554402aba1bd5fcd0` стартовал с `a030ce3`, завершился `succeeded`, `cleanup=done`, `quality.passed=true`. Worker создал `4dafb9d` для команды `hi`. В исходном Mattermost треде `jqor5w1qd3n5irizqd1jqued1a` Balda durable outbox хранит два разных отправленных сообщения с provider ID: запрос апрува (event 13) и финал (event 30). Повторы webhook шли с теми же dedupe keys; cleanup (event 31) не создал третий финальный пост. -- **Доставка кода:** после успеха Balda не вызвала `execution_publish_artifacts`. Коммит `4dafb9d` опубликован вручную через проверяющий API Broker в Git-зеркало и чистый локальный `/Users/admin/Developer/go-cli-test`. После публикации `go test ./...` прошёл, `go run . hi` напечатал `hello world!`, `add 2 3` — `5`. Причина пропуска найдена: MCP давал Balda ссылки на артефакты, но требовал недоступный ей SHA из `commit.json`; webhook prompt также не называл публикацию явно. Broker теперь сам проверяет commit/quality/bundle, а `expected_head` в MCP необязателен. Активный Broker обновлён, Balda webhook prompt требует публикацию перед закрытием Story, плагин обновлён до rc.19. Idempotent вызов действующего API без `expected_head` вернул HTTP 200 и прежнюю ревизию `4dafb9d`; новый автоматический end-to-end прогон ещё не проведён. -- **PR [#12](https://github.com/baldaworks/balda/pull/12):** открыт, не draft, HEAD `73e02156`; локальные проверки прошли, все 8 GitHub CI checks этого HEAD прошли, ревьюер запросил исправление ошибки HTTP 404/503, которое включено в новый коммит. Изменение маршрутизирует header-authenticated webhook по `chat_id`, даёт fallback для отсутствующей/неактивной сессии и receipt после реальной отправки. Чистый checkout PR находится на `73e02156`. Прежний вывод о другом SHA Balda был неверен: `d094943b` в Go metadata относился к внешнему репозиторию Broker, а `deepseekfix` был унаследованной меткой старого образа. Теперь Balda запущена на образе `balda-mm:pr12-73e02156-20261003`, собранном из чистого текущего HEAD PR #12; хеш запущенного бинарника совпадает с build artifact `7673f939…`. Текущий конфиг прошёл `balda validate`, плагин rc.19 установлен. Ревьюер `metalgman` оставил `CHANGES_REQUESTED` из-за HTTP 404 при временном сбое чтения session. Исправление отправлено в PR коммитом `73e02156`: 404 остаётся только для `ErrSessionUnavailable`, прочие ошибки resolver дают 503, HTTP-регрессия добавлена. Локально `go test -race ./...` и `go tool golangci-lint run` прошли; все 8 checks нового HEAD прошли. После CI нужен повторный сквозной прогон. -- **Следующий контроль:** проверить автоматическую публикацию без ручного API-вызова в новом прогоне и повторить уведомления на образе из `c5186037`. PR и Broker-изменения не слиты автоматически. +Balda ведёт исходную беседу и durable waits; Broker управляет admission, одноразовым worker, credentials, deadline и артефактами. Callee выполняет закреплённый workflow через выбранный ACP executor. Prism проецирует факты выполнения в локальные Story/Task Beads. Прикладные iPad/UI-сценарии и права Obsidian относятся к вызывающему проекту и Balda. ---- +## Подтверждено -# Live state — Broker/Balda workspace run — 2026-10-02 21:42 Бишкек +- Оба Balda обновлены, общие инструменты и policy выровнены; Eagle дополнительно имеет Asana readonly. Модели, аккаунты и история сохранены. +- Native runtime archives проверены на Darwin/Linux ARM64/AMD64, включая физический Intel Mac с Monterey и физический Linux x86_64. +- Generic tooling для Make/C, Python, Node, JVM и Go, immutable repository cache, artifact inputs и opt-in environment checkpoints реализованы и проверены. +- Реальная Mattermost-беседа прошла owner approval через Telegram, outage/restart, возврат в ту же сессию, проверенные артефакты и cleanup. +- Linux Claude/OpenCode прошли реальные Story и reuse/restart. Codex прошёл две полные Story на свежих macOS VM, включая restart Broker/issuer. Тесты из артефактов выполнены независимо. +- Read-only permission-handler Claude/OpenCode отклоняет инъецированные ACP-запросы записи; тестовый файл не создаётся. У OpenCode native plan policy не менялась. +- Исправленные macOS bootstrap развёрнуты; прежние архивы и конфиг сохранены для отката. Подготовлен защищённый GPG escrow; шесть тестов используют синтетические ключи. -Статус: **live Balda → Broker продвинут дальше: local-git checkout, Docker network, status webhook и writable workspace root исправлены; нужен повторный Mattermost `/execute` для end-to-end приёмки. Public stable ещё NO-GO.** +## Открытые границы -## Что произошло +| Область | Условие завершения | +| --- | --- | +| Linux Codex | Совместимый nested sandbox при сохранённой изоляции Docker worker; сейчас namespace creation завершается отказом | +| Recovery рабочих аккаунтов | Независимый recovery public key или secret store, off-host escrow и проверенное восстановление реальных аккаунтов; рабочие ключи пока не экспортированы | +| Provider failure lifecycle | Реальные expiry/upstream revocation и повторная авторизация на отдельных тестовых identity; рабочую Claude-сессию отзывать запрещено | +| Codex read-only | Отдельная проверка фактического отказа записи и итоговый production-прогон закреплённого bootstrap | -Пользователь гонял Mattermost-задачу `go-cli-test/go-cli-test2` через Balda. Первые отказы были не пользовательскими: Broker требовал HTTPS origin/allowlist, Docker worker не мог checkout локальный Broker Git remote, OpenCode+DeepSeek не был доступен в Linux-container профиле, статусы не возвращались в текущий thread, а `developer` root был смонтирован в Broker как read-only. Это live-инцидент интеграции, а не ошибка пользователя. +Задачи и их статусы ведутся в локальном Beads/Prism, корневая Story `eb-71b7971ac3fdd03b8ff6b198`. Этот документ задаёт границы приёмки, а не второй backlog. Контракты и команды — в `docs/work-plan.md`, `docs/support.md`, `docs/recovery.md`, `docs/executors.md` репозитория Broker. -## Исправлено в live - -1. **Local Git checkout.** Для `/local-git/` URL Runner больше не делает shallow fetch, потому что dumb HTTP не поддерживает `--depth=1`; local Git object writer переведён на SHA-1. Проверенный local-git revision для `go-cli-test`: `b6ea84fcbacdd509aec7ce43efe1fe3aa044abe0`. -2. **Docker/OpenCode/DeepSeek.** Добавлен Linux worker profile `linux-opencode-deepseek`; worker умеет создавать disposable контейнеры в `execution-broker_control`, чтобы они видели Broker как `http://broker:7791`. Образ worker/executor обновлён до `sha256:5dd416c40c69821c53f9cd5436238cc145da9ea0c746a03fb282004914ecfe55`. -3. **Status delivery.** Broker добавлен в обе сети `execution-broker_control` и `balda-mm-mac_default`; Balda config получил webhook `/webhook/execution-acceptance-current` на текущий Mattermost thread `7bjw4g7o1jdsbmjbqd649xxgaw:c96xswjbufnj7rtm3exny81xwc`; Broker notification route временно wildcard `chat_id="*", topic_id="*"` на этот webhook, чтобы не зависеть от смены Mattermost session id. -4. **Workspace root creation.** Live override `state/deploy/compose.chat-login.yaml` исправлен с `/Users/admin/Developer:/workspace-roots/Developer:ro` на `rw`; Broker container пересоздан вручную с writable mount. Проверено внутри Broker: `/workspace-roots/Developer` показывает `RW=true`, временная папка создаётся и удаляется. -5. **Container topology.** `execution-broker-broker-1` слушает `:7791`, `execution-broker-worker-1` в `execution-broker_control`, `balda-mm-mac` в `balda-mm-mac_default`; Broker имеет alias `broker` в обеих сетях. - -## Проверено - -1. `git fetch` + `git checkout` из disposable executor container по `http://broker:7791/local-git/0f18a21313cad59068830293b7817883.git` на commit `b6ea84fcbacdd509aec7ce43efe1fe3aa044abe0` прошли успешно. -2. Изнутри Broker успешно создана и удалена probe-папка в `/workspace-roots/Developer`; ошибка `read-only file system` для создания `go-cli-test2` должна быть снята. -3. `env GOCACHE=/private/tmp/eb-go-cache go test ./internal/store ./internal/config ./internal/worker` прошёл. -4. Unit tests `internal/broker` на notification path не были полностью прогнаны в sandbox: `httptest` bind на loopback запрещён текущим окружением; это test-environment limitation, не runtime-доказательство. - -## Открыто прямо сейчас - -1. Повторить из Mattermost: `@balda /execute в репо go-cli-test2 собери Go CLI проект через Docker + OpenCode + DeepSeek`. -2. Убедиться, что Balda больше не просит ручной `mkdir`, HTTPS origin или ручной status; ожидаемый flow: request/grant folder access → local managed Git remote → `execution_start` → Docker/OpenCode/DeepSeek run → terminal status в той же теме. -3. Если следующий run снова падает, смотреть новый `run_id`, Broker events и Balda webhook logs; не ретраить старый failed run. -4. После успешной live-приёмки убрать временный wildcard notification route и заменить его нормальной маршрутизацией статуса по фактическому Mattermost session/thread. -5. Закрепить все live hotfixes в репозиторной конфигурации/compose, чтобы `docker-compose up` не вернул read-only mount или старые network settings. - -## Файлы/настройки, затронутые live-фиксами - -- `/Users/admin/Developer/execution-broker/internal/runner/runner.go` — non-shallow fetch для local Git remotes. -- `/Users/admin/Developer/execution-broker/internal/broker/repository_local.go` — SHA-1 Git object IDs. -- `/Users/admin/Developer/execution-broker/internal/config/config.go` — `container_network`, wildcard notification validation и workspace config fields. -- `/Users/admin/Developer/execution-broker/internal/worker/worker.go` — worker-created containers получают configured Docker network. -- `/Users/admin/Developer/execution-broker/internal/store/store.go` — wildcard notification lookup. -- `/Users/admin/Developer/execution-broker/deploy/compose.yaml` — Broker alias `broker` в control/balda networks. -- `/Users/admin/Developer/execution-broker/state/deploy/broker.json` — live worker image/network и notification route. -- `/Users/admin/Developer/execution-broker/state/deploy/worker.json` — live worker image/network. -- `/Users/admin/Developer/execution-broker/state/deploy/compose.chat-login.yaml` — Developer root теперь `rw` для Broker. -- `/Users/admin/Docker/balda-mm-mac/config/config.yaml` — добавлен webhook route `execution_acceptance_current`. - ---- - -# Balda + Prism/Beads + Execution Broker + Callee + Codex / Claude / OpenCode - -> Обновлено: 2026-10-02 после push ветки workspace. -> Статус: **локальный Docker/OpenCode сценарий принят; live Balda → Broker → Prism/Mattermost и public stable ещё NO-GO.** Удалённая ветка `codex/broker-workspace-repos` проверена на GitHub: HEAD `f237556` (основная реализация `7eda0d6`). Удалённый `main` — `cb9d54b`; workspace-ветка в него не слита. -> Канонический смежный проект Virfield: [[personal/projects/virfield]]. Исторические результаты ниже сохраняются как хронология; текущие статус и приоритеты — в этом блоке. - -### Текущий handoff — создание локального репозитория Broker - -**Реализовано и запушено:** MCP `execution_repository_create` и HTTP `POST /v1/workspaces` создают идемпотентный Git-репозиторий под настроенным `workspace_root` (на этом Mac — `/Users/admin/Developer`). Balda выбирает короткий `name_hint` сама, передаёт текущие `chat_id/topic_id`, получает `workspace://` и полный commit SHA, затем передаёт их в `execution_start`. Нельзя просить пользователя заранее создавать или называть репозиторий. Gateway проверяет принадлежность chat/topic, чистый checkout и точный revision; одноразовый worker получает mount только конкретного repo в `/workspace`. Docker worker остаётся с отдельным образом установленного OpenCode/Callee/Runner; issuer разрешает workspace IDs только при явном `--workspace-repositories`. Плагин содержит инструкцию Balda использовать новый инструмент. Код и тесты находятся в удалённой ветке `codex/broker-workspace-repos`. - -**Проверено локально:** отдельный Broker/gateway/issuer создали `workspace://balda-go-cli-d427a5a9a939` в `~/Developer/balda-go-cli-d427a5a9a939`. Synthetic Story `run-7d10077b345641df09bb9af459c2dd7e` с OpenCode/DeepSeek прошёл роли, Human approval, verifier (9 выполненных проверок), reviewer, quality gate, 6 SHA-проверенных артефактов и cleanup `done`; финальный run — `succeeded`. В репозитории локальный commit `dbb2519`, checkout чистый, remote не добавлялся. Отдельная сборка `docker build --network=none` из созданного Dockerfile прошла на хосте; образ напечатал `Hello, Ada!`, пустое имя завершилось с ожидаемым кодом 2. `make check` прошёл в изолированной рабочей копии `state/isolated/broker-workspace-repos`. Тестовый стек остановлен, credential lease освобождён. - -**Граница доказательства:** это прямой изолированный вызов Broker, не сообщение Balda из Mattermost и не Prism Story. Живые Broker/gateway/Balda на новый workspace-профиль и MCP tool не переключались. Пушнутая workspace-ветка основана на RC9 `6091a61`, а проверенные Docker/OpenCode runtime-исправления находятся отдельно в локальном `codex/claude-release-fixes` (`5792cff`): в запушенном Dockerfile worker пока нет Go/Python, `GOTMPDIR` и нужных runtime-настроек. Проверенный успешный прогон использовал собранный локально образ с этими исправлениями. Поэтому одного push `f237556` недостаточно для live-развёртывания. В логах успешного Story встречался ACP `fs/write_text_file: Method not found`; на результате этого прогона не сказалось, но совместимость ACP требует проверки. - -**Следующие шаги:** - -1. Свести workspace-ветку с проверенными Docker/OpenCode изменениями `5792cff`, закрепить workflow digest и worker image ID, прогнать `make check` и Docker Story на точном объединённом commit. -2. Развернуть этот commit в live Broker/gateway: отдельный `linux-container` профиль с `workspace_root=/Users/admin/Developer`, mount корня только в gateway, workspace scope в issuer; обновить managed plugin Balda и проверить, что она видит `execution_repository_create`. -3. Попросить Balda в Mattermost создать новый проект **без имени repo и без готового checkout**. Проверить фактическую цепочку: MCP create → Prism Story → `execution_start` → Human approval в той же теме → события/артефакты в Mattermost/Prism → terminal cleanup и чистый локальный Git commit. -4. После live-приёмки проверить ACP-ошибку, CI точного SHA, release/rollback и оставшиеся security gates; только затем считать public stable готовым. - -## 1. Цель - -Balda принимает задачу в Mattermost, ведёт Story через Prism/Beads и запускает выполнение через Execution Broker. Класс выполнения выбирается по задаче: обычная разработка — изолированный Docker worker; Xcode и macOS API — одноразовая VM Virfield; изменение управляемого хоста — отдельный capability lease. - -```text -Mattermost → Balda → Broker → Docker gateway → disposable Linux worker → Callee → OpenCode/Claude/Codex ACP - ↘ Virfield → macOS VM → Runner → Callee → ACP -Balda → Prism adapter → bd → Dolt/Beads -ACP result → Callee event → Broker event log → Balda → Prism/Beads + Mattermost -``` - -При создании локального проекта Broker/Gateway создаёт Git-репозиторий в настроенном каталоге Mac. В worker монтируется только этот репозиторий; Docker socket, credential vault и весь каталог `~/Developer` внутрь worker не передаются. Для macOS VM действуют отдельные правила без host shared folders. - -## 2. Подтверждённый статус и оставшаяся интеграция - -### Balda v0.4.2 + RC10 plugin — 2026-10-02, 01:08 Бишкек - -- Рабочая Balda обновлена с `0.1.16-session-feedback` до официального `v0.4.2` (`86f9139`) через локальный образ из SHA-проверенного release binary; прежний образ и полный snapshot состояния сохранены для отката. Плагин обновлён с `rc.6` до `0.1.0-rc.10`. После restart: `balda validate: ok`, Mattermost transport configured, ошибок запуска нет. GitHub Actions и push не выполнялись. -- Первый пользовательский `/execute` вернул `Unknown command`: Balda CLI установила файлы плагина, но не активировала его в managed-каталоге. После второй полной резервной копии официальный startup-import Balda активировал RC10. В live DB: enabled plugin, 1 command, 1 skill, 0 diagnostics; snapshot содержит `execute`, Mattermost запущен без ошибок. Повторная пользовательская команда ещё нужна для end-to-end приёмки. Старые сессии сохраняют прежний каталог возможностей. Детали и резервные копии: `/Users/admin/Developer/execution-broker/state/private-publication-audit-20261001/release-readiness-rc10.md`. -- Повторный пользовательский `/execute` распознан, но после пяти 30-секундных таймаутов модели попал в deadletter; Broker run не стартовал. Старое имя `deepseek-chat` официально выведено из эксплуатации. У того же ключа доступны `deepseek-flash` и `deepseek-v4-pro`; Flash не завершил короткие пробы, Pro дал ответ за 8,6 с и tool call за 6,9 с. Тестовая Balda теперь использует Pro и документированный URL через локальную сборку `v0.4.2+deepseekfix`: timeout 180 с, режим размышления для DeepSeek выключен. Adapter tests и `balda validate` прошли, плагин RC10 активен, Mattermost переподключён. Это локальный hotfix, не официальный бинарник; свежая end-to-end команда ещё нужна. - -### RC10 OpenCode permission и локальный релиз-кандидат — 2026-10-01, 23:20 Бишкек - -- Чистый publication commit `ef1af48d33bf4e25d053c8d5d1fab62d5afa9c8b` создан только локально. GitHub Actions и push не запускались по решению оператора. Исходники проверены (`make check` в обоих checkout, Callee race, publication scan 215 файлов). Локальный release kit из пяти архивов прошёл SHA256 и exact-SHA проверку; Darwin ARM64 принят без Go, Linux ARM64 запущен без сети и записи. Доказательства: `/Users/admin/Developer/execution-broker/state/private-publication-audit-20261001/release-readiness-rc10.md`. Это не CI/signing evidence. -- Предыдущий OpenCode build/deny тест реально выполнил запрещённую команду и экспортировал созданный файл. После исправления ACP diagnostic `run-3d420f795cdfcedac909b75167ded1b6` записал отказ `tool_kind=execute`; файл и patch отсутствовали, VM удалена. Штатный read-only OpenCode теперь использует `plan/deny` со строгим policy, пишущий — `build/allow` через ACP; остальные пары отклоняются до inference. -- Обычный `macos-opencode-deepseek` профиль обновлён на credential-free bootstrap policy-v5 и workflow digest `sha256:f24146978e3c2a8792d98ab5d960e3032e51aa93f20dda3505e43f090f55a355`. Live Broker обновлён на локальный образ из точного RC10 релизного бинарника; `/healthz` вернул `0.1.0-rc.10`. Full Story `run-bc593b5eab0c18c49f2a16b395387b53` прошёл шесть ролей, Human approval, пять выполненных проверок verifier, review, quality gate, restart Broker/issuer, checkpoint и cleanup; шесть артефактов SHA-проверены. DeepSeek vault свободен, VM pool 2/2 свободен. Mattermost-сообщений не отправлялось. -- Public stable по-прежнему не объявлен: CI отложен, а Docker sandbox/live provider, upstream revoke, Mattermost-origin и реальные managed-host проверки остаются открытыми. - - - -### Проверка chat login — 2026-10-01, 13:36 Бишкек - -- Clean publication HEAD `4dc0615`, исходная локальная repo `5875e5a`, ветка `codex/multi-executor-profiles`, PR https://github.com/mallexxx/execution-broker/pull/1 (draft/private). Изменения запушены и синхронизированы в Developer. Полный `make check`, реальный synthetic Keychain probe/checkpoint и Gitleaks по полной публикационной истории прошли. Оба CI-запуска нового HEAD `4dc0615` завершились успешно (branch и PR). -- Реализован полноценный `claude auth login --claudeai`: profile/inference scopes, native access/refresh cache, привязка account+organization при checkpoint. Старый setup-token сохраняется до успешного обновления. Terminal handle очищается; остаётся только несекретная метка формата. Existing Codex vault не изменялся. -- v2 сохранил static OAuth, создал VM и запустил первую роль; server-managed organization policy отклонила setup-token. Cleanup/checkpoint исправлены и подтверждены. v3 `run-36c7622122d50c626c17c3e913acf3a7` завершился `authentication_failed`, cleanup `done`: временный HOME скрывал системный Keychain. Это воспроизведённая ошибка окружения, а не подтверждённая задержка пользователя или expiration. -- Исправление Keychain развернуто в issuer helper и worker bootstrap: системный HOME нужен macOS Security.framework, CLAUDE_CONFIG_DIR и CLAUDE_SECURESTORAGE_CONFIG_DIR остаются отдельными для login/run. Default Keychain/search-list не меняются. Проверены native write/read/auth status/export/delete и чтение обновлённого synthetic cache Runner с private HOME; эти проверки не заменяют реальный network refresh/inference. -- Balda создал v4 Story `eb-73afda6e242885a0ff4503e3`, run `run-558545102ec40e23c6562eb2c4bf85fd`, key `chat-login-claude-account-20261001-v4`. Human auth доставлен в существующую тестовую тему; native browser consent уже завершён, полная account session сохранена в encrypted vault. Тот же run возобновился автоматически. Блокировка `image_in_use` снята соседней задачей; VM выделена, Claude ACP прошёл session initialization. Первая роль `specify` завершилась без final text (`callee_failed`, peer connection closed); точная причина ещё не установлена. Run failed, артефакты сохранены, cleanup done. Успешный Claude inference пока не принят. Образ и работу соседней задачи не меняли. UI automatic redirect, native local callback без copy/paste, success/failure состояния реализованы. Неверная версия Балди про expiration v3 исправлена в теме и ограничена правилом в plugin skill. - -### Проверка Claude onboarding и ACP — 2026-10-01, 14:58 Бишкек - -- Для валидированной Claude account-сессии Runner теперь восстанавливает `hasCompletedOnboarding` вместе с OAuth account metadata. Development commit `2cee913`, чистая публикация `f10d103`; полный `make check` прошёл. Credential-free bootstrap с новым Runner развернут в обычный `macos-claude`; Broker доступен. -- Полный Claude Story повторно не прошёл: v5 `run-dfcd654b35e5e2dfeccf9e738f914ae4`, v6 `run-dd66bb66eacb1bda7b59a170775ea071` и warm-up Story `run-a7b2982dcf494c463f595944db4869a6` остановились в первом `specify` без final text. Все VM удалены. Диагностический Callee подтвердил пустые provider error code/message при полном prompt: `run-04fe781d89f9ccc3c172b7677e8c67b1`. -- Короткие native CLI и ACP `specify` прошли в одной VM `run-8fc39c972340f894b30405aa06008a52`. Тот же полный prompt отдельно прошёл native Claude CLI в новой VM `run-b8f4f3970264eeffadafbc0040849692`: exit 0, непустой ответ, cleanup done. Лог title generation error был также в успешном ACP probe, поэтому сам по себе он не доказывает отказ auth. Похоже на сбой ACP session lifecycle на содержательных запросах; первопричина не установлена. Профиль Claude не принят для full Story. Временные диагностические профили удалены из Broker. Соседний образ Virfield не менялся. -- Текущий кандидат — `0.1.0-rc.5`, clean publication commit `12a5add`. Канонический комплект из четырёх платформенных CI-архивов, plugin архива и SHA256SUMS загружен в приватный draft release. Все пять SHA256 совпали с CI, проверки прошли нативно на четырёх платформах, распакованные файлы прошли publication/Gitleaks. Локальные платформенные архивы отличаются из-за версии Go и дополнительных лицензий из кеша и не используются для публикации. Clean-machine install и Mattermost first-command остаются открытыми. Remote Git ref уже `12a5add`; [CI для точного SHA](https://github.com/mallexxx/execution-broker/actions/runs/36842265997) прошёл все 14 jobs через отдельный нерелизный validation tag. Private draft PR #1 всё ещё показывает прежний head `f10d103`, поэтому его старые checks не используем как доказательство RC5. Подробная запись: `/Users/admin/Developer/execution-broker/state/private-publication-audit-20261001/claude-onboarding-20261001.md`. Решение по RC5 и полный список release gates: `/Users/admin/Developer/execution-broker/state/private-publication-audit-20261001/release-readiness-rc5.md`. - -Balda PR [#11](https://github.com/baldaworks/balda/pull/11) смержен в `main` 2026-10-01 15:00 Бишкек (`86f9139`); восемь CI checks успешны. Это закрывает upstream gate для исправления session-preparation feedback, но установленная Balda требует отдельной сверки с merge commit. - -Production/stable release остаётся незакрытым: Claude Story и повторная свежая VM после рестарта issuer/Broker теперь проходят без входа (v4 `run-bd291c97e5e098ed2e2ca57b876a0b1a`, обычный профиль `run-1039e027ec5c7862367cb95a7fc02a51`; оба quality gate passed, cleanup done, Prism completed). Код исправления — dev commit `05e7c44`; RC5 draft устарел. Открыты provider-side expiration/revocation, OpenCode login/inference, Docker/remotes credential lifecycle и ранее перечисленные full-stack gates. Credentials и login capabilities в этой заметке не сохраняются. - -### RC9 Docker credential lifecycle — 2026-10-01, 19:26 Бишкек - -- Dev commit `74a10f2`, чистый publication commit `6091a610992c83abdf706c3721d2c5d75c117e56` запушен в приватный PR #1. Docker worker теперь получает credential от host issuer при первом start после durable admission, checkpoint-ит остановленный Runner до удаления container/volume и освобождает identity только после подтверждённого cleanup. Потерянный worker или отклонённый checkpoint удерживает владельца до явного revoke/recovery. Remote-host fixed plans остаются отдельными без provider credentials. -- Синтетические DeepSeek-тесты покрывают reuse в двух свежих Docker run, запрет замены API key, capacity refusal без утечки owner, lost-worker revoke cleanup и private gateway checkpoint RPC. Полный `make check` в dev и publication checkout прошёл; publication scan — 214 файлов. Live Docker provider Story и Codex sandbox на совместимом хосте не приняты. -- Локальный RC9 release kit точного SHA собран для Darwin/Linux ARM64/AMD64 и plugin; все 5 SHA256SUMS совпали. Darwin ARM64 check-release прошёл в fresh HOME без Go в PATH (7 бинарников, vault import/revoke, Broker MCP/auth, SQLite backup); Linux ARM64 семь бинарников запустились в изолированном контейнере без сети. Это локальные артефакты, не exact-CI и не подпись. -- Push CI `36868572492` и PR CI `36868579280` для точного RC9 SHA завершились до первого шага: GitHub annotation указывает failed account payments или spending limit. RC9 CI assets и подписанной provenance нет; public release остаётся no-go. OpenCode/DeepSeek disposable run `run-ab12be94116626bc5e2ed8d42889d7b1` завершился `succeeded`, patch пустой, cleanup done, vault owner null; модель в plan mode прямо сообщила, что не вызывала `touch`, поэтому tool-level denial ещё не принят. Второй build-mode/permissions-deny bootstrap подготовлен, но новая Virfield image operation снова держит `available=0`. Временный профиль удалён, обычный Broker `healthz=200`. - -### RC8 retry/backoff fix — 2026-10-01, 19:00 Бишкек - -- В OpenCode denied-write probe Virfield вернул `image_in_use` из-за отдельной image_build операции, которая при лимите 2 VM намеренно устанавливает `available=0` для всех новых leases. Run был отменен до получения VM; чужая загрузка не прерывалась. Обнаружен Broker bug: промежуточная выдача credential snapshot сбрасывала exponential backoff, поэтому issuer выполнял частые issue/abort циклы. Исправлено в dev `f937ccc`, добавлен regression test; полный `make check` прошёл. -- Live Broker image собран из точного RC8 commit и развернут только для Broker. Синтетический run `run-594708d118987f24a406c9410d3814ae` во время Virfield image lock показал retry_count 0 → 1 → 2, затем был отменён с `cleanup=done`; DeepSeek inference и Mattermost-сообщений не было. Evidence: `/Users/admin/Developer/execution-broker/state/private-publication-audit-20261001/rc8-live-backoff-probe.json`. -- Дополнительный Linux ARM64 архив RC8 точного SHA собран локально; все семь бинарников запустились в контейнере без сети, Go и provider credentials. Это не exact-CI артефакт. Docker Codex в credential-free диагностике прошёл дальше только при отключённых seccomp и AppArmor, затем упёрся в `RTM_NEWADDR: Operation not permitted`; production изоляция не менялась. -- Соседний Virfield image job `job-a56dffdf456ecf269dc7c18bce963a97` после полной загрузки IPSW и установки macOS перешёл в `needs_attention` на `assistant_dispatched` с `image_command_failed`. Общий maintenance lock остаётся, `available=0`; OpenCode denied-write VM ждёт восстановления владельцем Virfield job. -- Clean publication RC8 `6984bc67e4a9b991ae66acf06d91578ffcff586b` запушен в приватный PR. Push CI `36863848919` и PR CI `36863854456` не стартовали из-за GitHub Billing & plans, подтверждено annotation. Локальный Darwin ARM64 архив точного commit прошёл binary-only check-release с Go вне PATH и SHA256SUMS, но не является exact-CI release asset. Public stable no-go. -- Docker Engine 29.2.1 диагностирован отдельно: узкий seccomp профиль разрешает только user namespace и сохраняет запрет mount/network namespaces, но полный Codex bwrap sandbox всё ещё не запускается; даже одноразовый `seccomp=unconfined` дошёл до `Failed to make / slave: Permission denied`. Production worker flags не ослаблены. -- Подробный ledger: `/Users/admin/Developer/execution-broker/state/private-publication-audit-20261001/release-readiness-rc8.md`. - -### RC7 exact-SHA gate — 2026-10-01, 18:35 Бишкек - -- Clean publication commit `5c213bb98b0caaa24bc7bb07ba73bd8589bdd07a` запушен в существующий приватный PR #1. Его push CI `36862025838` и PR CI `36862030297` не выполнили ни одного шага: GitHub annotation сообщает failed account payments или недостаточный spending limit. Старый RC6 CI не является доказательством для RC7. Обновление Billing & plans необходимо до публикации exact-CI release assets. -- Локально в чистом checkout прошли `make check`, Callee race test, проверка 213 публичных файлов, plugin version `0.1.0-rc.7`. Локальный Darwin ARM64 RC7 архив из точного SHA прошёл `check-release.py` в чистом временном состоянии без Go в PATH, включая версии, vault, Broker MCP/auth и SQLite backup. Это локальный архив без подписи, не замена native CI artifacts. -- Подробный release ledger: `/Users/admin/Developer/execution-broker/state/private-publication-audit-20261001/release-readiness-rc7.md`. Public stable по-прежнему no-go. - -### OpenCode DeepSeek acceptance — 2026-10-01, 18:25 Бишкек - -- Существующий DeepSeek provider Balda использован без вывода ключа: ключ через private pipe импортирован в отдельный зашифрованный `opencode-deepseek-work` vault. Нативный OpenCode 1.18.34 вернул `OK` на реальном `deepseek/deepseek-flash` запросе. Credential-free VM bootstrap и постоянный issuer на `127.0.0.1:7796` установлены; рабочий профиль `macos-opencode-deepseek` подключен к Broker. -- Два полных синтетических Story (`run-cf83a0a3a0768d45f82199ae8db4176e`, `run-6b12f6189bc2e9b32fccadb596fc06b5`) прошли шесть ролей, Human approval, quality gate, шесть SHA-256-verified artifacts, Prism completion и cleanup. Второй запустился в новой VM и пережил перезапуск Broker/issuer на approval без browser login. -- Между ними был отказ `run-c6d2008abe6308000375c2a4e55c0c3e` только из-за добавленного моделью поля `notes` в валидный отчет verifier. Узкое исправление parser в dev `396eb5a` принимает `notes`, но всё ещё отвергает другие неизвестные поля и неисполненные/ошибочные проверки; Callee race и полный `make check` прошли. Для публикации нужен новый exact-SHA кандидат. -- OpenCode denied-write probe не дошёл до VM: Virfield зарезервирован соседней операцией image export (`image_in_use`). Проверочный run отменен с cleanup done, временный профиль удален. Upstream revocation/expiry ещё не проверены; static DeepSeek API key не имеет OAuth refresh. - -### RC6 candidate и актуальная приёмка — 2026-10-01, 17:25 Бишкек - -- Clean publication commit `2d786c4`, [CI 36854040694](https://github.com/mallexxx/execution-broker/actions/runs/36854040694) 14/14, приватный [draft RC6](https://github.com/mallexxx/execution-broker/releases/tag/untagged-4a40999515a9662c914c): четыре native CI-архива, plugin archive и SHA256SUMS побайтно сверены. GitHub draft SHA256 совпадают с локальным bundle. Remote main ещё не обновлён, публичного релиза нет. -- Claude full Story прошёл в двух свежих macOS VM, включая обычный профиль после restart issuer/Broker без browser login; reviewer/verifier/quality gate passed, cleanup done, Prism completed. Запрещённая запись в read-only роли отклонена. Managed plugin `0.1.0-rc.6` установлен в рабочую Balda из CI архива. -- Оставшиеся gates, доказательства и решение no-go: `/Users/admin/Developer/execution-broker/state/private-publication-audit-20261001/release-readiness-rc6.md`. До их закрытия RC6 остаётся приватным draft. - - -Базовый релиз Virfield v2.0.0 опубликован. На Mac установлен более новый versioned-images/GHCR кандидат, который ещё не принят как production release. Актуальные Git/runtime, матрица golden/clone E2E и полный оставшийся план поддерживаются только в [[personal/projects/virfield]]. Сервисы используют deployment вне checkout; публикация архива и коммит исходников не обновляют runtime автоматически. - -Локально подтверждено 2026-09-30: - -- отдельный `execution-broker` repo и sidecar deployment; доступ из реального Balda container через MCP; -- Virfield v2 lease → pinned SSH tunnel → Runner/Callee; чистый clone выполняет XCTest и отдаёт xcresult до cleanup; -- полный Codex workflow с шестью свежими ролями и Human approval; реальный локальный commit и пять unittest проходят; -- persistent OAuth vault/Keychain и host issuer: две последовательные свежие VM работают без нового browser login, checkpoint сохраняется до удаления VM; последняя проходит quality gate, restart Broker/issuer и Story completion; -- Prism/Beads projection и local Story lifecycle. Доказательства: `/Users/admin/Developer/execution-broker/state/private-publication-audit-20261001/acceptance.md`. - -До production/public stable release остаются Mattermost-origin/delivery, совместимый Docker Codex sandbox, приёмка credential lifecycle на Docker/remotes, реальные managed-host identities/TLS/rollback, согласованное восстановление всего стека, remote CI/clean-machine distribution/signing. Встроенные роли пока sequential; параллельные worktrees и модельные бюджеты не реализованы. Полный аудит: `/Users/admin/Developer/execution-broker/state/private-publication-audit-20261001/production-review.md`. - -Статус остальных проектов и provider-конфигурация проверяются в их собственных источниках; релиз Virfield не является подтверждением их готовности. - -Подтверждено 2026-10-01: - -- Чистый publication repo: `/Users/admin/Developer/execution-broker-public`, remote `https://github.com/mallexxx/execution-broker` (private), коммит `cb9d54b8369bfd7437b98ccf76b9ff3d2df397f5`. Частная development history не отправлена. Аудитные истории перенесены из public docs в private state. -- CI `36815717157`: 10/10 успешных заданий. Нативное исполнение Darwin/Linux ARM64/AMD64; Linux дополнительно устанавливается из архива без Go, собирает четыре образа, проходит real Callee/Beads Human, рестарт Broker/Prism, проверку пяти артефактов и cleanup. Пять именно CI-архивов и SHA256SUMS загружены в private draft `v0.1.0-rc.4`; подписи отсутствуют. -- Managed plugin `execution-broker` 0.1.0-rc.4 установлен через штатный Balda CLI. Старый standalone skill сохранён вне discovery roots. Файл Balda с MCP credentials имеет mode 0600; OAuth vault/Keychain сохранены. -- Balda PR 10 принят upstream. Исправление bounded user-visible preparation failure отправлено в PR 11; локальные race/lint/architecture проверки прошли. Рабочая Balda обновлена до `0.1.16-session-feedback`, подключение Mattermost подтверждено. Первый PR 11 CI выявил отдельную гонку старого restart-теста; исправлена в `e0e953bc`, 50 повторов под race detector прошли. Финальный PR 11 CI `36817575956`/`36817575952`: 8/8 успешных проверок. PR открыт; upstream merge ещё не выполнен. - -Весь стек нельзя объявлять полностью production-ready: свежий Mattermost-origin запуск нового managed `/execute` ещё не принят; Docker provider sandbox/автоматический OAuth на Docker и remotes, реальные host identities/TLS/rollback, per-role worktrees/model budgets и официальное подписанное распространение остаются отдельными незакрытыми границами. Имеющаяся приёмка Mattermost recovery и drained backup/restore не заменяет эти проверки. - - -### Дополнение 2026-10-01: Claude CLI и OpenCode - -В целевой объём включены Claude CLI и OpenCode с несколькими моделями. Balda выбирает allowlisted agent_profile из каталога Broker; executor/model задаются оператором. Секреты не входят в конфиг профиля, плагин, репозиторий или VM image. Callee остаётся владельцем workflow, а профиль не может ослабить authored permissions. - -Реализовано в ветке `codex/multi-executor-profiles`, clean publication commit `35cbe3ad5710f8eb5a52147da068ac016fb137c6`: - -- отдельные encrypted identities/Keychain accounts с backward compatibility существующего Codex vault; типизированные cache leases, проверка executor/provider/account и сохранение обновлённых credentials до освобождения VM; -- automated enrollment: Claude official setup-token через private PTY без печати токена; OpenCode auth login в отдельном HOME/XDG с импортом только выбранного provider; API-key ввод скрыт; браузерное согласие остаётся одноразовым действием человека; -- integrity-locked optional runtimes: Claude Code 2.1.286, Claude ACP 0.84.0, OpenCode 1.18.34; конфигурации и workflow packs, выбор модели, поддержка в каталоге и skill Balda; backup проверяет все identities; -- локально прошли race/vet/build, Callee, credential/enrollment/backup/publication checks; реальный credential-free CLI/ACP probe на macOS ARM64 прошёл, npm audit и полный clean-history Gitleaks без находок. CI на четырёх платформах: https://github.com/mallexxx/execution-broker/actions/runs/36820468335 — 14/14 успешных заданий; native optional executor probe прошёл на всех четырёх платформах. Draft PR: https://github.com/mallexxx/execution-broker/pull/1. - -Это ещё не принятые production-профили: нужны реальные Claude/OpenCode login/inference, повторная свежая VM без логина, expiration/revocation, проверка read-only режима и полный Balda Story. OpenCode OAuth без стабильного accountId отклоняется; Anthropic subscription OAuth не переносится в OpenCode. Автоматический issuer для Docker/remotes по-прежнему не реализован. RC4 и live Codex deployment не изменены. - -Документация: `/Users/admin/Developer/execution-broker/docs/executors.md`. Подготовлены private launchers `/Users/admin/Developer/execution-broker/state/executor-enrollment/enroll-claude.command` и `enroll-opencode.command`; пока не запускались и не обращались к действующим credentials. - -## 3. Владельцы ответственности - -### Balda — оркестратор верхнего уровня - -Balda: - -- принимает задачу и уточнения из Mattermost; -- создаёт и обновляет Story в Prism/Beads; -- выбирает разрешённый workflow по его идентификатору; -- запускает выполнение через Broker; -- показывает запросы Human и итог пользователю. - -Balda не управляет процессами внутри VM напрямую. Она работает только через контракты Prism adapter и Execution Broker. - -### Prism adapter + Beads — долговременное состояние - -Prism adapter принадлежит контуру Balda и физически может быть отдельным sidecar-контейнером. - -Он: - -- предоставляет Balda узкие операции Story/Task/phase; -- вызывает `bd`; -- подключается к Dolt/Beads database в соседнем контейнере; -- обеспечивает идемпотентную запись событий Broker; -- остаётся единственным писателем предметного состояния задачи. - -Broker, Callee и Codex не пишут в Beads напрямую. Они возвращают факты выполнения, а Balda решает, как отразить их в Story. - -### Execution Broker — инфраструктурный исполнитель - -Broker — отдельный проект и отдельный репозиторий `execution-broker`. В production он живёт отдельным sidecar-контейнером рядом с Balda. - -Он: - -- получает от Balda команду запуска; -- арендует VM через Virfield; -- запускает VM runner и Callee; -- хранит техническое состояние run и журнал событий; -- передаёт ввод, отмену и ссылки на артефакты. - -Broker не выбирает workflow, не исполняет его граф и не меняет Prism/Beads. - -### Virfield — граница управления хостом - -Virfield работает на Mac-хосте и владеет жизненным циклом VM: - -- clone/start/stop/delete; -- IP и SSH readiness; -- сетевые туннели; -- lease и принудительная уборка VM. - -Аутентифицированный HTTP API `/api/v1` и HTTP MCP `/mcp` реализованы; daemon слушает loopback. Для Broker остаются проверяемая сетевая доступность и integration contract. Произвольного host shell в API нет. - -### Callee и Codex — выполнение внутри VM - -Callee исполняет workflow: фазы, ветвления, циклы, Human-паузы и запуск ролей. - -`implementer`, `reviewer`, `verifier` — роли Callee. Каждая роль запускает новую Codex ACP-сессию с отдельным prompt и контекстом. - -Codex изменяет код, запускает `xcodebuild`, тесты и другие команды внутри VM. Codex не запускает Callee и не управляет Story. - -## 4. Prism и prism-callee - -Официальный `prism-callee` — плагин к coding-agent host. В штатной схеме он делает coding agent владельцем Beads и запускателем Callee: - -```text -Codex → prism-callee skill → Callee → дочерние Codex sessions -``` - -Эта штатная топология не является целевой, потому что верхним оркестратором должен оставаться Balda. - -В целевой схеме используются отдельно: - -- lifecycle и правила Prism — как основа skill/адаптера Balda; -- Callee workflow pack — как версионируемые workflow-файлы внутри VM; -- Beads — через отдельный Prism adapter; -- Codex — только как исполнитель ролей Callee. - -До реализации требуется проверить, какие части официального плагина можно импортировать без изменений, а какие нужно адаптировать для Balda. - -## 5. Прямой data flow - -1. Пользователь даёт Balda задачу в Mattermost без готового репозитория. -2. Balda читает `execution_profiles`, выбирает `workspace_enabled` Docker worker и вызывает `execution_repository_create` со стабильным idempotency key, текущими `chat_id/topic_id` и собственным коротким `name_hint`. -3. Broker через gateway создаёт локальный Git-репозиторий под настроенным workspace root и возвращает `workspace://` ID с полным commit SHA; Balda создаёт Prism Story. -4. Balda вызывает `execution_start` с этим ID/revision, `story_id`, теми же chat/topic и allowlisted workflow/agent profile. -5. Broker durable-записывает run, gateway монтирует только выбранный repo в одноразовый Linux worker, Runner запускает Callee и установленный ACP-исполнитель. Для Xcode вместо Docker используется отдельная VM Virfield. -6. События, Human response, артефакты и cleanup идут через Broker; Prism/Beads и Mattermost обновляет Balda. - -Пример полей запуска после создания репозитория: - -```json -{ - "idempotency_key": "story-42-attempt-1", - "story_id": "story-42", - "chat_id": "mattermost-chat-id", - "topic_id": "mattermost-topic-id", - "workflow_id": "opencode/story", - "workflow_version": "sha256:<точный digest установленного workflow>", - "repository": "workspace://broker-created-name", - "revision": "<полный Git commit SHA>", - "runtime_class": "linux-container", - "worker_profile": "docker-dev", - "agent_profile": "opencode-deepseek", - "task": "Добавить Go CLI и тесты", - "ttl_seconds": 3600 -} -``` - -Broker возвращает `run_id` после надёжной записи команды; выполнение продолжается асинхронно. - -## 6. Обратный data flow - -1. Codex возвращает результат роли в Callee по ACP. -2. Callee переводит workflow в следующее состояние и отдаёт структурированное событие VM runner. -3. VM runner передаёт событие Broker; Broker сохраняет его до ответа Balda. -4. Balda читает события через `execution_events(run_id, after_event_id)` и подтверждает курсор. -5. Balda обновляет Story через Prism adapter и публикует значимые изменения в Mattermost. - -События доставляются как минимум один раз. Повторная доставка безопасна благодаря уникальному `(run_id, event_id)`. - -Минимальное событие: - -```json -{ - "event_id": 17, - "run_id": "run-123", - "story_id": "bd-42", - "type": "role_completed", - "phase": "review", - "role": "reviewer", - "status": "passed", - "summary": "Tests pass", - "artifacts": [ - {"kind": "git_commit", "ref": "abc123"}, - {"kind": "xcresult", "ref": "artifact://run-123/tests"} - ] -} -``` - -Тяжёлые логи и `.xcresult` не помещаются в журнал. Broker хранит только ссылки, checksum, размер и срок жизни. - -## 7. Human approval - -1. Callee доходит до Human node и приостанавливает workflow. -2. VM runner отправляет `human_required` с вопросом и вариантами. -3. Broker фиксирует состояние `waiting_human`. -4. Balda показывает вопрос в Mattermost и получает ответ в том же session/topic. -5. Balda вызывает `execution_respond`; Broker доставляет ответ в конкретный paused run. - -Связка обязательна: `chat_id/topic_id ↔ story_id ↔ run_id ↔ human_request_id`. - -Ответ должен быть идемпотентным: повторный `human_request_id` не возобновляет workflow дважды. - -## 8. Execution Broker: контракт - -### Где живёт - -Отдельный контейнер в одном Docker network с Balda. У Broker собственный persistent volume с SQLite. - -Northbound: HTTP MCP для Balda. -Southbound: Docker gateway для `linux-container`, Virfield API и scoped SSH для `macos-vm`, отдельный host-runner для `remote-host`. -Внутри одноразового worker: Runner → Callee → выбранный ACP-исполнитель. - -### Основные MCP tools - -1. `execution_profiles` — workflow, worker и agent allowlist. -2. `execution_repository_create` — идемпотентно создать новый локальный Git repo для chat/topic. -3. `execution_start`, `execution_status`, `execution_events`, `execution_ack` — durable run и события. -4. `execution_respond`, `execution_cancel`, `execution_artifacts` — Human response, отмена и артефакты. -5. `execution_remote_plan` и отдельные Virfield image tools — управляемые host/VM сценарии. - -### Состояния run - -```text -accepted -→ provisioning -→ booting -→ running -→ waiting_human -→ running -→ succeeded | failed | canceled | expired -``` - -Каждый переход записывается транзакционно вместе с событием. После рестарта Broker продолжает незавершённые run или переводит их в явно диагностируемое состояние. - -### Внутреннее состояние Broker - -Broker хранит только технические данные: - -- `run_id`, `story_id`, idempotency key; -- VM id, IP, lease deadline; -- workflow id/version и process/session id; -- event cursor, terminal status и error code; -- artifact references и cleanup status. - -### VM runner - -Нужен небольшой процесс внутри образа VM. Он отделяет Broker от консольного формата Callee. - -Runner: - -- проверяет входной manifest и allowlist workflow; -- запускает Callee в рабочей директории репозитория; -- преобразует lifecycle/output в стабильные JSON events; -- принимает cancel и Human response; -- возвращает terminal result и artifact manifest. - -Broker не должен парсить человекочитаемый ANSI/TTY-вывод Callee. Если Callee уже имеет стабильный machine-readable API, runner становится тонким адаптером. - -## 9. Execution workers и распределение ресурсов - -Broker планирует не только macOS VM. Все среды исполняют один versioned Runner protocol, но имеют разные `runtime_class`: - -- `linux-container` — обычный код, CLI, Go build/test и статическая проверка Dockerfile; Docker socket в worker не передаётся, фактическая сборка образа требует отдельного host/builder шага; -- `macos-vm` — Xcode, macOS APIs, GUI и UI tests; -- `remote-host` — изменение конкретного хоста после Human approval. - -Минимальный WorkerSpec: - -```json -{ - "runtime_class": "macos-vm", - "worker_profile": "xcode-27", - "agent_profile": "codex-default", - "workflow_id": "prism/story", - "repository": "git@host:org/repo.git", - "revision": "main" -} -``` - -### Параллельные задачи - -Balda поддерживает несколько независимых chat sessions. Turns внутри одной session сериализуются, разные sessions могут работать параллельно. - -Callee поддерживает `Parallel`, но это fan-out Role visits внутри одного workflow. VM создаёт и распределяет Broker, не Balda и не Callee. - -Политика пула: - -1. Одна изменяющая macOS Story получает одну эксклюзивную VM. -2. Implementer, reviewer и verifier используют ту же VM, но свежие ACP-сессии. -3. Отдельная VM на каждый Role не создаётся. -4. Одновременно работают максимум две macOS VM; остальные run стоят в очереди. -5. Linux containers масштабируются отдельно и не занимают macOS VM slots. - -Параллельные read-only роли могут работать в одной VM. Параллельные изменения получают отдельные Git worktrees; запись нескольких агентов в один checkout запрещена политикой Runner. - -Ограничение в две macOS VM задаёт Apple Virtualization.framework, а не Balda. Практический лимит дополнительно зависит от RAM и CPU. - -## 10. Изоляция, shared folder и секреты - -Для `macos-vm` основная граница — одноразовая VM: исходники и Xcode build живут на её локальном диске, без host shared folders. Virfield выдаёт scoped lease/SSH, а VM удаляется после выгрузки результатов. - -Для `linux-container` граница — одноразовый worker с read-only root, UID 65532, ограниченными capabilities и отдельным `/state`. Gateway имеет Docker socket и mount настроенного workspace root; сам worker получает только bind mount одного проверенного локального Git repo в `/workspace`. Broker и Balda не получают этот host mount. Запуск с `workspace://` требует `workspace_enabled`, соответствия chat/topic, точного commit SHA и чистого checkout. Credential issuer выдаёт scoped lease только после durable admission, checkpoint и cleanup завершают владение. Docker CLI/socket внутри worker отсутствуют. - -Ни Balda, ни модель не получают пользовательский `~/.ssh`, credential vault или произвольный host shell. - -### Credential management — обязательный scope (2026-09-30) - -OAuth-вход выполняется один раз при enrollment или при необходимости повторной авторизации у провайдера. Перезапуск или пересоздание VM сам по себе не должен требовать нового входа. - -Владелец provider credentials — отдельный host credential issuer Execution Broker. Virfield управляет VM/SSH leases, но не хранит OAuth пользователя. Balda, Prism/Beads и Callee-события получают только состояние; токены в их сообщения не передаются. - -Обязательные свойства: - -- долговременное зашифрованное хранилище вне VM и Git; на macOS master key находится в Keychain; права 0700/0600, atomic write и fsync; -- выдача в конкретный run, repository и agent profile после durable admission; приватный файл в HOME гостя, не в checkout, environment, argv или golden/bootstrap; -- одна OAuth identity арендуется одним run одновременно, чтобы VM не конкурировали за refresh token; ownership переживает restart Broker/issuer; -- перед release VM обновлённый cache сохраняется обратно через приватный канал только после остановки ролей; блокировка снимается после подтверждённого удаления VM; -- local revoke, provider-side revoke как отдельная операция, безопасное восстановление после утраты VM, отсутствие токенов в публичных API/артефактах; TTL Broker не считается upstream revocation. - -Приёмка: новый clone выполняет реальные Codex роли без браузерного login; Broker/issuer переживают restart; checkpoint сохраняется до cleanup; следующий clone использует сохранённую сессию. Отдельно тестируются конфликт владельца, повреждение vault, отказ refresh, revoke и невозможность восстановить отозванный токен поздним checkpoint. Потерянный checkpoint блокирует повторную выдачу до явного восстановления, а не молча использует старый token. - -Реализация и границы текущей версии: `/Users/admin/Developer/execution-broker/docs/credentials.md`. Live macOS приёмка завершена: `run-f94853e932ef9b620a28412b609c62c7`, все 6 ролей и quality gate пройдены, 5 unittest, cleanup done, encrypted checkpoint generation 6. Активный профиль Balda — `macos-codex` + `prism/story`; enrollment-only профиль отключён. Поддержка автоматического issuer сначала включена для macOS VM; Docker/remotes требуют отдельной приёмки того же жизненного цикла. OAuth может требовать нового входа после provider revoke/refresh rejection; это не штатный шаг каждого запуска VM. - -### Workspace и артефакты - -Текущий recipe Virfield создаёт VM без shared host folders. Код, mutable workspace и Xcode build живут на локальном диске VM; перенос исходников/результатов — через scoped SSH и Git либо `rsync/scp`. Артефакты выгружаются до release lease и получают уникальный путь `run_id/...`. - -Добавление host mounts потребует отдельного решения по изоляции и проверки согласованности данных; это не часть принятого recipe. - -### Изменение других хостов - -Задачи настройки Mac, Kraken, TrueNAS и других машин используют `runtime_class: remote-host`. - -1. Agent в контейнере формирует plan и выполняет read-only preflight. -2. Balda показывает точный diff действий и запрашивает Human approval. -3. Broker выдаёт capability lease на конкретные host, identity, operations и TTL 15–30 минут. -4. Runner применяет изменения через dedicated service account или установленный host-runner. -5. Verify, audit log и rollback закрывают lease. - -Личные ключи из пользовательского `~/.ssh` не передаются. Для managed host используется небольшой Go host-runner. Для первичного bootstrap допускается отдельный SSH account с short-lived certificate и ограниченным `sudo`. - -Изменения самого Mac выполняет отдельный native launchd helper. Он принимает подписанный capability lease; Balda и модель не получают постоянный host shell. - -## 11. Неподтверждённое - -### Блокирует прототип - -1. Есть ли у Callee стабильный machine-readable event stream и программный resume Human node. -2. Можно ли запустить workflow pack `prism/story` отдельно от stock plugin host. -3. Как Codex ACP получает workspace/cwd и сохраняет его между Role visits. -4. Как передавать cancel в Callee без повреждения terminal result. -5. Как подключить Broker к реализованному Virfield API через TLS/routing и проверить lease/SSH contract из его окружения. - -### Требует проверки до production - -- credential-free установка Codex; отдельное долговременное credential management с выдачей в каждую disposable VM (авторизация в golden image запрещена); -- доступность Dolt/Beads из Prism adapter после рестартов; -- формат и размер Xcode артефактов; -- восстановление Broker после падения во время `waiting_human`; -- параллельные Story и лимиты VM/CPU/RAM на Mac. - -## 12. Варианты - -### A. Sidecar Broker + VM runner — выбран - -Balda остаётся оркестратором, Broker изолирует инфраструктуру, Callee владеет workflow, Codex работает в VM. Это целевая схема. - -### B. Broker как host daemon - -Меньше сетевых переходов к Virfield и SSH, но Broker получает более широкую поверхность хоста. Использовать только если контейнерный sidecar не может надёжно управлять VM. - -### C. Stock prism-callee plugin - -`Codex → plugin → Callee → Codex`. Быстрее для проверки Prism, но Balda перестаёт быть владельцем lifecycle. Подходит как временный reference test, не как production topology. - -### D. Native remote ACP - -Будущая transport abstraction в `runtime + acpagent` позволит Balda или Callee подключаться к ACP по WebSocket/Streamable HTTP. Она может заменить SSH-транспорт, но не заменяет Broker, VM lifecycle, durable events и cleanup. - -## 13. Virfield v2 — реализовано и опубликовано - -Релиз [v2.0.0](https://github.com/mallexxx/virfield/releases/tag/v2.0.0) опубликован 2026-09-30, commit `ee3aa1d`. Текущая карточка — [[personal/projects/virfield]]; команды и контракты принадлежат документации репозитория. - -Реализованы единый Go control plane, SQLite journal, reconciliation, leases/idempotency, resource admission, image pipeline, scoped SSH, cleanup, CLI/MCP и embedded console. Лимит двух VM и неизвестные состояния обрабатываются явно; неопределённая мутация не повторяется автоматически. Personal SSH keys/PAT storage и client-side lifecycle fallback в текущей реализации отсутствуют. - -Исторический релиз v2.0.0 использовал Lume 0.5.3; его CI и golden/clone acceptance относятся к release commit 2026-09-30. С 2026-10-08 установленный Virfield использует Tart + SSH nodes. В hardening-кандидате 2026-10-14 Lume backend/factory/wrapper/build/install code удалён; старый отдельный Lume LaunchDaemon отключён и выгружен 2026-10-14 после проверки пустого inventory. Новый кандидат ещё не принят для релиза; текущие Tart/factory live результаты записаны отдельно. Текущие evidence и gates — в [[personal/projects/virfield]] и [Verification](https://github.com/mallexxx/virfield/blob/codex/retire-lume-release-hardening/docs/VERIFICATION.md). - -### Граница проектов и репозиториев — принятое решение - -`virfield` и `execution-broker` — два независимых проекта. Их нельзя объединять в один репозиторий или один daemon. - -Репозиторий `virfield` содержит host control plane: корневой Go module, `cmd/virfieldd`, `cmd/virfield`, `cmd/virfield-mcp`, реализации в `internal/`, deployment в `deploy/`. Актуальная карта модулей — [Architecture](https://github.com/mallexxx/virfield/blob/codex/retire-lume-release-hardening/docs/ARCHITECTURE.md); отдельного каталога приложения v2 нет. - -Целевая структура отдельного `execution-broker` (реализацию сверять в его репозитории): - -```text -cmd/broker -cmd/runner -cmd/host-runner -internal/scheduler -internal/execution -internal/events -internal/drivers/virfield -internal/drivers/container -internal/drivers/remotehost -api/ -``` - -Границы ответственности: - -1. `virfield` управляет VM, images, leases и host-level cleanup; он ничего не знает о Callee, Prism, Beads и model providers. -2. `execution-broker` выбирает execution class, запускает Runner/Callee, управляет agent profiles и хранит run event log; он не управляет Lume напрямую. -3. `balda` вызывает Broker по MCP и отражает события в Prism/Beads; Virfield напрямую из Balda не вызывается. -4. `execution-broker → virfieldd` использует versioned authenticated HTTP API `/api/v1`; текущие модели и маршруты — `internal/domain`, `internal/httpapi`, `internal/client`. Отдельная OpenAPI-спецификация Virfield в v2.0.0 не опубликована. -5. Общих Go-пакетов между репозиториями нет; совместимость обеспечивается API contract и contract tests. - -## 14. Agent profiles и модельная политика - -Broker и Runner не содержат vendor-specific workflow logic. Run выбирает `agent_profile` из статического allowlist. - -Для Callee напрямую подтверждены Codex, Claude, Copilot, Grok, OpenCode и Cursor. Gemini и GLM подключаются через `generic_acp` или отдельный ACP adapter и считаются неподтверждёнными до integration test. - -Worker image содержит Runner, Callee и нужные provider binaries. Credentials выбранного provider выдаются только на срок run. Node отсутствует в Virfield/Broker control plane; он допускается внутри специализированного worker image, если конкретный agent CLI без него не работает. - -### Распределение моделей - -Схема `Balda → Callee → coding agent` остаётся правильной. Provider для Balda и профили исполнителей настраиваются в соответствующих проектах; конкретное назначение не фиксируется в плане Virfield. - -Рекомендуемая раскладка: - -- Balda — надёжное понимание запроса, выбор workflow, управление state и Human interaction; -- specify/design/review — сильная reasoning-модель; -- implementation — Codex или другой сильный coding agent; -- build/test/SSH apply — детерминированные Script/Runner operations; -- дешёвая модель — routing, summarization и простая диагностика. - -DeepSeek подходит для ограниченного routing и простых операций с жёсткими tool schemas. Он не принимает самостоятельные решения о privileged SSH-командах и сложной архитектуре. - -Оптимизация стоимости строится на deterministic routing, token/run budgets, bounded retries и escalation: после повторной неудачи задача повышает model tier, а не повторяет дешёвую модель бесконечно. - -## 15. Pitfalls - -### Архитектура - -1. Смешивание двух топологий: stock `Codex → Callee` и целевой `Balda → Broker → Callee`. -2. Перенос workflow-логики в Broker превращает его во второй оркестратор. -3. Прямая запись Broker/Callee/Codex в Beads создаёт несколько владельцев состояния. -4. Запуск Callee вне VM переносит Script nodes и часть команд на хост. -5. Парсинг консольного вывода Callee вместо стабильного event protocol ломает resume и диагностику. - -### Надёжность - -1. Потеря события между Callee и Balda без durable event log. -2. Двойной запуск VM без idempotency key. -3. Повторный Human response возобновляет workflow дважды. -4. Падение Broker оставляет VM без lease и cleanup. -5. Большие build-логи забивают SQLite или MCP-ответы. - -### Безопасность - -1. Непроверенный `workflow_id`, repository URL или revision превращается в выполнение произвольного ввода. -2. Общий SSH/Git credential между VM расширяет последствия компрометации. -3. Секреты в prompt, events или build logs попадают в Mattermost/Beads. -4. Host path, переданный как ACP cwd, не существует или указывает не туда внутри VM. -5. VM с доступом в локальную сеть может атаковать хостовые сервисы; egress и host API нужно ограничить. - -### Исполнение и ресурсы - -1. Mutable build через virtiofs может прочитать stale content; workspace должен жить на локальном диске worker. -2. Более двух активных macOS VM невозможны; Broker обязан иметь очередь и admission control. -3. Parallel Callee roles, пишущие в один checkout, создают file conflicts даже внутри одной VM. -4. Свободный provider command или agent-generated `cmd` превращает профиль в arbitrary code execution. -5. Установка всех provider credentials в один golden image нарушает isolation и усложняет rotation. - -## 16. План работ после push `f237556` - -Предыдущие этапы Broker/Runner, Virfield v2 и credential lifecycle выполнены частично и описаны выше как история. Текущий критический путь относится к новому локальному repo и Docker/OpenCode: - -1. **Свести исходники и принять точный build.** Ветка `codex/broker-workspace-repos` (`f237556`) основана на RC9, поэтому соединить её с Docker/OpenCode runtime-исправлениями `5792cff`; зафиксировать plugin version, workflow digest, Runner/Callee и immutable worker image ID. Проверить `make check` и повторить изолированный Story на этом exact commit. -2. **Развернуть live контур.** Настроить выделенный Docker worker profile, `workspace_root=/Users/admin/Developer`, mount корня только в gateway и `--workspace-repositories` у issuer; установить новую версию Broker/gateway и managed plugin Balda. Проверить MCP discovery и отсутствие регрессии действующих VM профилей. -3. **Принять пользовательский Mattermost flow.** Сообщение Balda не содержит repo name/path. Она сама вызывает `execution_repository_create`, создаёт Prism Story, запускает run, доставляет Human approval и события в исходный chat/topic, публикует артефакты и фиксирует cleanup. Проверить Git commit в новом repo и отсутствие двойного создания при повторе idempotency key. -4. **Закрыть release gates.** Разобрать наблюдённый ACP `fs/write_text_file: Method not found`, выполнить CI/publication checks на точном SHA, проверить rollback и оставшиеся credential/managed-host ограничения. Public stable объявлять только после live приёмки. - -### Дополнительные задачи — 2026-10-11: альтернативные аппрувы и ACP-контекст - -- [x] **Telegram для Broker workflows и worker ACP.** Реализованы привязка к run/Human/владельцу, выбор единственного канала запроса, возврат результата в исходную беседу, срок действия, обновление кнопок, restart recovery и competing-channel deduplication. Текущий контракт и доказательства: [[personal/projects/balda/execution-broker]]. -- [ ] **Прямые ACP-разрешения хостовых агентов Balda.** Единая межканальная доставка этих разрешений не покрывается Telegram-механизмом Broker; это отдельная незавершённая часть плана. -- [ ] **Прогон субагента для автоаппрува и сбора дополнительного контекста команд ACP-субагентов.** Перед решением по запросу разрешения запускать отдельного субагента: собрать цель команды, cwd, аргументы, затрагиваемые файлы/ресурсы, ожидаемые побочные эффекты и связь с текущей задачей. Использовать собранный контекст для автоаппрува в пределах настроенной политики; при недостатке данных или необходимости решения пользователя направлять запрос на ручной аппрув, включая Telegram. Сохранять решение и обоснование в событиях run. -- [ ] **Приёмочный прогон.** Проверить связку ACP-запрос → сбор контекста субагентом → автоаппрув либо ручной ответ через Telegram → возобновление нужного ACP-субагента. Покрыть недостаточный контекст, отказ, таймаут и повторный ответ. - -## 17. Критерии готовности - -1. Balda сама создаёт новый локальный Git repo под настроенным workspace root из обычной Mattermost-задачи; пользователь не передаёт заранее repo name/path. -2. Один chat/topic получает одну Prism Story и идемпотентный Broker run с полным pinned commit SHA, allowlisted workflow и agent profile. -3. Docker worker запускает установленный ACP-исполнитель без Docker socket и host secrets; Xcode/macOS задачи остаются в VM Virfield. -4. Human approval возвращается в исходную тему, события и артефакты доходят до Balda/Prism/Mattermost, terminal cleanup и credential checkpoint завершаются. -5. Повторный запрос/рестарт не создаёт второй repo или run; отказ, cancel и rollback дают диагностируемое состояние. -6. CI и release доказательства относятся к одному exact SHA объединённого кода; remote-host задачи требуют отдельный capability lease. - -## Связанные заметки - -- [[personal/projects/virfield]] — текущий релиз, подтверждённый scope и каноническая документация. - -- [[personal/tech/callee-prism]] — контракты Callee, ACP и Prism. +Исторические исследования и PR-дневник сохранены отдельно: [[personal/projects/archive/balda-callee-prism-plan-through-2026-10-14]]. Они не описывают текущую установку. diff --git a/personal/projects/archive/balda-callee-prism-plan-through-2026-10-14.md b/personal/projects/archive/balda-callee-prism-plan-through-2026-10-14.md new file mode 100644 index 00000000..700c7cac --- /dev/null +++ b/personal/projects/archive/balda-callee-prism-plan-through-2026-10-14.md @@ -0,0 +1,665 @@ +> [!WARNING] Исторический документ +> Не использовать как runbook текущей Balda. Актуальная локальная схема, пути и команды: [[personal/projects/balda/setup|Balda — актуальный локальный запуск]]. +> Текущий контейнер: `balda-mm-mac`; config: `/Users/admin/Docker/balda-mm-mac/config/config.yaml`; Obsidian MCP: `type: http`, `http://host.docker.internal:3101/mcp`. +> Материал ниже сохранён только как история. +> Актуальные результаты локального прогона 2026-10-14, статус кандидата rc.26 и незавершённые планы: [[personal/projects/balda/execution-broker|Execution Broker — текущий локальный контур]]. Старые NO-GO и SHA ниже относятся к указанным историческим запускам. + +# Текущее состояние — Balda / Broker — 2026-10-03 22:41 Бишкек + +Статус: **приёмочный Docker-прогон выполнен на прежнем образе; автоматическая публикация исправлена, а Balda запущена из текущего исправленного HEAD PR #12 (`73e02156`). Общий release остаётся NO-GO до нового end-to-end прогона, подтверждающего публикацию и уведомления на образе из текущего HEAD `73e02156`.** Записи ниже — история и могут содержать устаревшие статусы. + +- **Прогон:** `run-fbe2cb459fe2965554402aba1bd5fcd0` стартовал с `a030ce3`, завершился `succeeded`, `cleanup=done`, `quality.passed=true`. Worker создал `4dafb9d` для команды `hi`. В исходном Mattermost треде `jqor5w1qd3n5irizqd1jqued1a` Balda durable outbox хранит два разных отправленных сообщения с provider ID: запрос апрува (event 13) и финал (event 30). Повторы webhook шли с теми же dedupe keys; cleanup (event 31) не создал третий финальный пост. +- **Доставка кода:** после успеха Balda не вызвала `execution_publish_artifacts`. Коммит `4dafb9d` опубликован вручную через проверяющий API Broker в Git-зеркало и чистый локальный `/Users/admin/Developer/go-cli-test`. После публикации `go test ./...` прошёл, `go run . hi` напечатал `hello world!`, `add 2 3` — `5`. Причина пропуска найдена: MCP давал Balda ссылки на артефакты, но требовал недоступный ей SHA из `commit.json`; webhook prompt также не называл публикацию явно. Broker теперь сам проверяет commit/quality/bundle, а `expected_head` в MCP необязателен. Активный Broker обновлён, Balda webhook prompt требует публикацию перед закрытием Story, плагин обновлён до rc.19. Idempotent вызов действующего API без `expected_head` вернул HTTP 200 и прежнюю ревизию `4dafb9d`; новый автоматический end-to-end прогон ещё не проведён. +- **PR [#12](https://github.com/baldaworks/balda/pull/12):** открыт, не draft, HEAD `73e02156`; локальные проверки прошли, все 8 GitHub CI checks этого HEAD прошли, ревьюер запросил исправление ошибки HTTP 404/503, которое включено в новый коммит. Изменение маршрутизирует header-authenticated webhook по `chat_id`, даёт fallback для отсутствующей/неактивной сессии и receipt после реальной отправки. Чистый checkout PR находится на `73e02156`. Прежний вывод о другом SHA Balda был неверен: `d094943b` в Go metadata относился к внешнему репозиторию Broker, а `deepseekfix` был унаследованной меткой старого образа. Теперь Balda запущена на образе `balda-mm:pr12-73e02156-20261003`, собранном из чистого текущего HEAD PR #12; хеш запущенного бинарника совпадает с build artifact `7673f939…`. Текущий конфиг прошёл `balda validate`, плагин rc.19 установлен. Ревьюер `metalgman` оставил `CHANGES_REQUESTED` из-за HTTP 404 при временном сбое чтения session. Исправление отправлено в PR коммитом `73e02156`: 404 остаётся только для `ErrSessionUnavailable`, прочие ошибки resolver дают 503, HTTP-регрессия добавлена. Локально `go test -race ./...` и `go tool golangci-lint run` прошли; все 8 checks нового HEAD прошли. После CI нужен повторный сквозной прогон. +- **Следующий контроль:** проверить автоматическую публикацию без ручного API-вызова в новом прогоне и повторить уведомления на образе из `c5186037`. PR и Broker-изменения не слиты автоматически. + +--- + +# Live state — Broker/Balda workspace run — 2026-10-02 21:42 Бишкек + +Статус: **live Balda → Broker продвинут дальше: local-git checkout, Docker network, status webhook и writable workspace root исправлены; нужен повторный Mattermost `/execute` для end-to-end приёмки. Public stable ещё NO-GO.** + +## Что произошло + +Пользователь гонял Mattermost-задачу `go-cli-test/go-cli-test2` через Balda. Первые отказы были не пользовательскими: Broker требовал HTTPS origin/allowlist, Docker worker не мог checkout локальный Broker Git remote, OpenCode+DeepSeek не был доступен в Linux-container профиле, статусы не возвращались в текущий thread, а `developer` root был смонтирован в Broker как read-only. Это live-инцидент интеграции, а не ошибка пользователя. + +## Исправлено в live + +1. **Local Git checkout.** Для `/local-git/` URL Runner больше не делает shallow fetch, потому что dumb HTTP не поддерживает `--depth=1`; local Git object writer переведён на SHA-1. Проверенный local-git revision для `go-cli-test`: `b6ea84fcbacdd509aec7ce43efe1fe3aa044abe0`. +2. **Docker/OpenCode/DeepSeek.** Добавлен Linux worker profile `linux-opencode-deepseek`; worker умеет создавать disposable контейнеры в `execution-broker_control`, чтобы они видели Broker как `http://broker:7791`. Образ worker/executor обновлён до `sha256:5dd416c40c69821c53f9cd5436238cc145da9ea0c746a03fb282004914ecfe55`. +3. **Status delivery.** Broker добавлен в обе сети `execution-broker_control` и `balda-mm-mac_default`; Balda config получил webhook `/webhook/execution-acceptance-current` на текущий Mattermost thread `7bjw4g7o1jdsbmjbqd649xxgaw:c96xswjbufnj7rtm3exny81xwc`; Broker notification route временно wildcard `chat_id="*", topic_id="*"` на этот webhook, чтобы не зависеть от смены Mattermost session id. +4. **Workspace root creation.** Live override `state/deploy/compose.chat-login.yaml` исправлен с `/Users/admin/Developer:/workspace-roots/Developer:ro` на `rw`; Broker container пересоздан вручную с writable mount. Проверено внутри Broker: `/workspace-roots/Developer` показывает `RW=true`, временная папка создаётся и удаляется. +5. **Container topology.** `execution-broker-broker-1` слушает `:7791`, `execution-broker-worker-1` в `execution-broker_control`, `balda-mm-mac` в `balda-mm-mac_default`; Broker имеет alias `broker` в обеих сетях. + +## Проверено + +1. `git fetch` + `git checkout` из disposable executor container по `http://broker:7791/local-git/0f18a21313cad59068830293b7817883.git` на commit `b6ea84fcbacdd509aec7ce43efe1fe3aa044abe0` прошли успешно. +2. Изнутри Broker успешно создана и удалена probe-папка в `/workspace-roots/Developer`; ошибка `read-only file system` для создания `go-cli-test2` должна быть снята. +3. `env GOCACHE=/private/tmp/eb-go-cache go test ./internal/store ./internal/config ./internal/worker` прошёл. +4. Unit tests `internal/broker` на notification path не были полностью прогнаны в sandbox: `httptest` bind на loopback запрещён текущим окружением; это test-environment limitation, не runtime-доказательство. + +## Открыто прямо сейчас + +1. Повторить из Mattermost: `@balda /execute в репо go-cli-test2 собери Go CLI проект через Docker + OpenCode + DeepSeek`. +2. Убедиться, что Balda больше не просит ручной `mkdir`, HTTPS origin или ручной status; ожидаемый flow: request/grant folder access → local managed Git remote → `execution_start` → Docker/OpenCode/DeepSeek run → terminal status в той же теме. +3. Если следующий run снова падает, смотреть новый `run_id`, Broker events и Balda webhook logs; не ретраить старый failed run. +4. После успешной live-приёмки убрать временный wildcard notification route и заменить его нормальной маршрутизацией статуса по фактическому Mattermost session/thread. +5. Закрепить все live hotfixes в репозиторной конфигурации/compose, чтобы `docker-compose up` не вернул read-only mount или старые network settings. + +## Файлы/настройки, затронутые live-фиксами + +- `/Users/admin/Developer/execution-broker/internal/runner/runner.go` — non-shallow fetch для local Git remotes. +- `/Users/admin/Developer/execution-broker/internal/broker/repository_local.go` — SHA-1 Git object IDs. +- `/Users/admin/Developer/execution-broker/internal/config/config.go` — `container_network`, wildcard notification validation и workspace config fields. +- `/Users/admin/Developer/execution-broker/internal/worker/worker.go` — worker-created containers получают configured Docker network. +- `/Users/admin/Developer/execution-broker/internal/store/store.go` — wildcard notification lookup. +- `/Users/admin/Developer/execution-broker/deploy/compose.yaml` — Broker alias `broker` в control/balda networks. +- `/Users/admin/Developer/execution-broker/state/deploy/broker.json` — live worker image/network и notification route. +- `/Users/admin/Developer/execution-broker/state/deploy/worker.json` — live worker image/network. +- `/Users/admin/Developer/execution-broker/state/deploy/compose.chat-login.yaml` — Developer root теперь `rw` для Broker. +- `/Users/admin/Docker/balda-mm-mac/config/config.yaml` — добавлен webhook route `execution_acceptance_current`. + +--- + +# Balda + Prism/Beads + Execution Broker + Callee + Codex / Claude / OpenCode + +> Обновлено: 2026-10-02 после push ветки workspace. +> Статус: **локальный Docker/OpenCode сценарий принят; live Balda → Broker → Prism/Mattermost и public stable ещё NO-GO.** Удалённая ветка `codex/broker-workspace-repos` проверена на GitHub: HEAD `f237556` (основная реализация `7eda0d6`). Удалённый `main` — `cb9d54b`; workspace-ветка в него не слита. +> Канонический смежный проект Virfield: [[personal/projects/virfield]]. Исторические результаты ниже сохраняются как хронология; текущие статус и приоритеты — в этом блоке. + +### Текущий handoff — создание локального репозитория Broker + +**Реализовано и запушено:** MCP `execution_repository_create` и HTTP `POST /v1/workspaces` создают идемпотентный Git-репозиторий под настроенным `workspace_root` (на этом Mac — `/Users/admin/Developer`). Balda выбирает короткий `name_hint` сама, передаёт текущие `chat_id/topic_id`, получает `workspace://` и полный commit SHA, затем передаёт их в `execution_start`. Нельзя просить пользователя заранее создавать или называть репозиторий. Gateway проверяет принадлежность chat/topic, чистый checkout и точный revision; одноразовый worker получает mount только конкретного repo в `/workspace`. Docker worker остаётся с отдельным образом установленного OpenCode/Callee/Runner; issuer разрешает workspace IDs только при явном `--workspace-repositories`. Плагин содержит инструкцию Balda использовать новый инструмент. Код и тесты находятся в удалённой ветке `codex/broker-workspace-repos`. + +**Проверено локально:** отдельный Broker/gateway/issuer создали `workspace://balda-go-cli-d427a5a9a939` в `~/Developer/balda-go-cli-d427a5a9a939`. Synthetic Story `run-7d10077b345641df09bb9af459c2dd7e` с OpenCode/DeepSeek прошёл роли, Human approval, verifier (9 выполненных проверок), reviewer, quality gate, 6 SHA-проверенных артефактов и cleanup `done`; финальный run — `succeeded`. В репозитории локальный commit `dbb2519`, checkout чистый, remote не добавлялся. Отдельная сборка `docker build --network=none` из созданного Dockerfile прошла на хосте; образ напечатал `Hello, Ada!`, пустое имя завершилось с ожидаемым кодом 2. `make check` прошёл в изолированной рабочей копии `state/isolated/broker-workspace-repos`. Тестовый стек остановлен, credential lease освобождён. + +**Граница доказательства:** это прямой изолированный вызов Broker, не сообщение Balda из Mattermost и не Prism Story. Живые Broker/gateway/Balda на новый workspace-профиль и MCP tool не переключались. Пушнутая workspace-ветка основана на RC9 `6091a61`, а проверенные Docker/OpenCode runtime-исправления находятся отдельно в локальном `codex/claude-release-fixes` (`5792cff`): в запушенном Dockerfile worker пока нет Go/Python, `GOTMPDIR` и нужных runtime-настроек. Проверенный успешный прогон использовал собранный локально образ с этими исправлениями. Поэтому одного push `f237556` недостаточно для live-развёртывания. В логах успешного Story встречался ACP `fs/write_text_file: Method not found`; на результате этого прогона не сказалось, но совместимость ACP требует проверки. + +**Следующие шаги:** + +1. Свести workspace-ветку с проверенными Docker/OpenCode изменениями `5792cff`, закрепить workflow digest и worker image ID, прогнать `make check` и Docker Story на точном объединённом commit. +2. Развернуть этот commit в live Broker/gateway: отдельный `linux-container` профиль с `workspace_root=/Users/admin/Developer`, mount корня только в gateway, workspace scope в issuer; обновить managed plugin Balda и проверить, что она видит `execution_repository_create`. +3. Попросить Balda в Mattermost создать новый проект **без имени repo и без готового checkout**. Проверить фактическую цепочку: MCP create → Prism Story → `execution_start` → Human approval в той же теме → события/артефакты в Mattermost/Prism → terminal cleanup и чистый локальный Git commit. +4. После live-приёмки проверить ACP-ошибку, CI точного SHA, release/rollback и оставшиеся security gates; только затем считать public stable готовым. + +## 1. Цель + +Balda принимает задачу в Mattermost, ведёт Story через Prism/Beads и запускает выполнение через Execution Broker. Класс выполнения выбирается по задаче: обычная разработка — изолированный Docker worker; Xcode и macOS API — одноразовая VM Virfield; изменение управляемого хоста — отдельный capability lease. + +```text +Mattermost → Balda → Broker → Docker gateway → disposable Linux worker → Callee → OpenCode/Claude/Codex ACP + ↘ Virfield → macOS VM → Runner → Callee → ACP +Balda → Prism adapter → bd → Dolt/Beads +ACP result → Callee event → Broker event log → Balda → Prism/Beads + Mattermost +``` + +При создании локального проекта Broker/Gateway создаёт Git-репозиторий в настроенном каталоге Mac. В worker монтируется только этот репозиторий; Docker socket, credential vault и весь каталог `~/Developer` внутрь worker не передаются. Для macOS VM действуют отдельные правила без host shared folders. + +## 2. Подтверждённый статус и оставшаяся интеграция + +### Balda v0.4.2 + RC10 plugin — 2026-10-02, 01:08 Бишкек + +- Рабочая Balda обновлена с `0.1.16-session-feedback` до официального `v0.4.2` (`86f9139`) через локальный образ из SHA-проверенного release binary; прежний образ и полный snapshot состояния сохранены для отката. Плагин обновлён с `rc.6` до `0.1.0-rc.10`. После restart: `balda validate: ok`, Mattermost transport configured, ошибок запуска нет. GitHub Actions и push не выполнялись. +- Первый пользовательский `/execute` вернул `Unknown command`: Balda CLI установила файлы плагина, но не активировала его в managed-каталоге. После второй полной резервной копии официальный startup-import Balda активировал RC10. В live DB: enabled plugin, 1 command, 1 skill, 0 diagnostics; snapshot содержит `execute`, Mattermost запущен без ошибок. Повторная пользовательская команда ещё нужна для end-to-end приёмки. Старые сессии сохраняют прежний каталог возможностей. Детали и резервные копии: `/Users/admin/Developer/execution-broker/state/private-publication-audit-20261001/release-readiness-rc10.md`. +- Повторный пользовательский `/execute` распознан, но после пяти 30-секундных таймаутов модели попал в deadletter; Broker run не стартовал. Старое имя `deepseek-chat` официально выведено из эксплуатации. У того же ключа доступны `deepseek-flash` и `deepseek-v4-pro`; Flash не завершил короткие пробы, Pro дал ответ за 8,6 с и tool call за 6,9 с. Тестовая Balda теперь использует Pro и документированный URL через локальную сборку `v0.4.2+deepseekfix`: timeout 180 с, режим размышления для DeepSeek выключен. Adapter tests и `balda validate` прошли, плагин RC10 активен, Mattermost переподключён. Это локальный hotfix, не официальный бинарник; свежая end-to-end команда ещё нужна. + +### RC10 OpenCode permission и локальный релиз-кандидат — 2026-10-01, 23:20 Бишкек + +- Чистый publication commit `ef1af48d33bf4e25d053c8d5d1fab62d5afa9c8b` создан только локально. GitHub Actions и push не запускались по решению оператора. Исходники проверены (`make check` в обоих checkout, Callee race, publication scan 215 файлов). Локальный release kit из пяти архивов прошёл SHA256 и exact-SHA проверку; Darwin ARM64 принят без Go, Linux ARM64 запущен без сети и записи. Доказательства: `/Users/admin/Developer/execution-broker/state/private-publication-audit-20261001/release-readiness-rc10.md`. Это не CI/signing evidence. +- Предыдущий OpenCode build/deny тест реально выполнил запрещённую команду и экспортировал созданный файл. После исправления ACP diagnostic `run-3d420f795cdfcedac909b75167ded1b6` записал отказ `tool_kind=execute`; файл и patch отсутствовали, VM удалена. Штатный read-only OpenCode теперь использует `plan/deny` со строгим policy, пишущий — `build/allow` через ACP; остальные пары отклоняются до inference. +- Обычный `macos-opencode-deepseek` профиль обновлён на credential-free bootstrap policy-v5 и workflow digest `sha256:f24146978e3c2a8792d98ab5d960e3032e51aa93f20dda3505e43f090f55a355`. Live Broker обновлён на локальный образ из точного RC10 релизного бинарника; `/healthz` вернул `0.1.0-rc.10`. Full Story `run-bc593b5eab0c18c49f2a16b395387b53` прошёл шесть ролей, Human approval, пять выполненных проверок verifier, review, quality gate, restart Broker/issuer, checkpoint и cleanup; шесть артефактов SHA-проверены. DeepSeek vault свободен, VM pool 2/2 свободен. Mattermost-сообщений не отправлялось. +- Public stable по-прежнему не объявлен: CI отложен, а Docker sandbox/live provider, upstream revoke, Mattermost-origin и реальные managed-host проверки остаются открытыми. + + + +### Проверка chat login — 2026-10-01, 13:36 Бишкек + +- Clean publication HEAD `4dc0615`, исходная локальная repo `5875e5a`, ветка `codex/multi-executor-profiles`, PR https://github.com/mallexxx/execution-broker/pull/1 (draft/private). Изменения запушены и синхронизированы в Developer. Полный `make check`, реальный synthetic Keychain probe/checkpoint и Gitleaks по полной публикационной истории прошли. Оба CI-запуска нового HEAD `4dc0615` завершились успешно (branch и PR). +- Реализован полноценный `claude auth login --claudeai`: profile/inference scopes, native access/refresh cache, привязка account+organization при checkpoint. Старый setup-token сохраняется до успешного обновления. Terminal handle очищается; остаётся только несекретная метка формата. Existing Codex vault не изменялся. +- v2 сохранил static OAuth, создал VM и запустил первую роль; server-managed organization policy отклонила setup-token. Cleanup/checkpoint исправлены и подтверждены. v3 `run-36c7622122d50c626c17c3e913acf3a7` завершился `authentication_failed`, cleanup `done`: временный HOME скрывал системный Keychain. Это воспроизведённая ошибка окружения, а не подтверждённая задержка пользователя или expiration. +- Исправление Keychain развернуто в issuer helper и worker bootstrap: системный HOME нужен macOS Security.framework, CLAUDE_CONFIG_DIR и CLAUDE_SECURESTORAGE_CONFIG_DIR остаются отдельными для login/run. Default Keychain/search-list не меняются. Проверены native write/read/auth status/export/delete и чтение обновлённого synthetic cache Runner с private HOME; эти проверки не заменяют реальный network refresh/inference. +- Balda создал v4 Story `eb-73afda6e242885a0ff4503e3`, run `run-558545102ec40e23c6562eb2c4bf85fd`, key `chat-login-claude-account-20261001-v4`. Human auth доставлен в существующую тестовую тему; native browser consent уже завершён, полная account session сохранена в encrypted vault. Тот же run возобновился автоматически. Блокировка `image_in_use` снята соседней задачей; VM выделена, Claude ACP прошёл session initialization. Первая роль `specify` завершилась без final text (`callee_failed`, peer connection closed); точная причина ещё не установлена. Run failed, артефакты сохранены, cleanup done. Успешный Claude inference пока не принят. Образ и работу соседней задачи не меняли. UI automatic redirect, native local callback без copy/paste, success/failure состояния реализованы. Неверная версия Балди про expiration v3 исправлена в теме и ограничена правилом в plugin skill. + +### Проверка Claude onboarding и ACP — 2026-10-01, 14:58 Бишкек + +- Для валидированной Claude account-сессии Runner теперь восстанавливает `hasCompletedOnboarding` вместе с OAuth account metadata. Development commit `2cee913`, чистая публикация `f10d103`; полный `make check` прошёл. Credential-free bootstrap с новым Runner развернут в обычный `macos-claude`; Broker доступен. +- Полный Claude Story повторно не прошёл: v5 `run-dfcd654b35e5e2dfeccf9e738f914ae4`, v6 `run-dd66bb66eacb1bda7b59a170775ea071` и warm-up Story `run-a7b2982dcf494c463f595944db4869a6` остановились в первом `specify` без final text. Все VM удалены. Диагностический Callee подтвердил пустые provider error code/message при полном prompt: `run-04fe781d89f9ccc3c172b7677e8c67b1`. +- Короткие native CLI и ACP `specify` прошли в одной VM `run-8fc39c972340f894b30405aa06008a52`. Тот же полный prompt отдельно прошёл native Claude CLI в новой VM `run-b8f4f3970264eeffadafbc0040849692`: exit 0, непустой ответ, cleanup done. Лог title generation error был также в успешном ACP probe, поэтому сам по себе он не доказывает отказ auth. Похоже на сбой ACP session lifecycle на содержательных запросах; первопричина не установлена. Профиль Claude не принят для full Story. Временные диагностические профили удалены из Broker. Соседний образ Virfield не менялся. +- Текущий кандидат — `0.1.0-rc.5`, clean publication commit `12a5add`. Канонический комплект из четырёх платформенных CI-архивов, plugin архива и SHA256SUMS загружен в приватный draft release. Все пять SHA256 совпали с CI, проверки прошли нативно на четырёх платформах, распакованные файлы прошли publication/Gitleaks. Локальные платформенные архивы отличаются из-за версии Go и дополнительных лицензий из кеша и не используются для публикации. Clean-machine install и Mattermost first-command остаются открытыми. Remote Git ref уже `12a5add`; [CI для точного SHA](https://github.com/mallexxx/execution-broker/actions/runs/36842265997) прошёл все 14 jobs через отдельный нерелизный validation tag. Private draft PR #1 всё ещё показывает прежний head `f10d103`, поэтому его старые checks не используем как доказательство RC5. Подробная запись: `/Users/admin/Developer/execution-broker/state/private-publication-audit-20261001/claude-onboarding-20261001.md`. Решение по RC5 и полный список release gates: `/Users/admin/Developer/execution-broker/state/private-publication-audit-20261001/release-readiness-rc5.md`. + +Balda PR [#11](https://github.com/baldaworks/balda/pull/11) смержен в `main` 2026-10-01 15:00 Бишкек (`86f9139`); восемь CI checks успешны. Это закрывает upstream gate для исправления session-preparation feedback, но установленная Balda требует отдельной сверки с merge commit. + +Production/stable release остаётся незакрытым: Claude Story и повторная свежая VM после рестарта issuer/Broker теперь проходят без входа (v4 `run-bd291c97e5e098ed2e2ca57b876a0b1a`, обычный профиль `run-1039e027ec5c7862367cb95a7fc02a51`; оба quality gate passed, cleanup done, Prism completed). Код исправления — dev commit `05e7c44`; RC5 draft устарел. Открыты provider-side expiration/revocation, OpenCode login/inference, Docker/remotes credential lifecycle и ранее перечисленные full-stack gates. Credentials и login capabilities в этой заметке не сохраняются. + +### RC9 Docker credential lifecycle — 2026-10-01, 19:26 Бишкек + +- Dev commit `74a10f2`, чистый publication commit `6091a610992c83abdf706c3721d2c5d75c117e56` запушен в приватный PR #1. Docker worker теперь получает credential от host issuer при первом start после durable admission, checkpoint-ит остановленный Runner до удаления container/volume и освобождает identity только после подтверждённого cleanup. Потерянный worker или отклонённый checkpoint удерживает владельца до явного revoke/recovery. Remote-host fixed plans остаются отдельными без provider credentials. +- Синтетические DeepSeek-тесты покрывают reuse в двух свежих Docker run, запрет замены API key, capacity refusal без утечки owner, lost-worker revoke cleanup и private gateway checkpoint RPC. Полный `make check` в dev и publication checkout прошёл; publication scan — 214 файлов. Live Docker provider Story и Codex sandbox на совместимом хосте не приняты. +- Локальный RC9 release kit точного SHA собран для Darwin/Linux ARM64/AMD64 и plugin; все 5 SHA256SUMS совпали. Darwin ARM64 check-release прошёл в fresh HOME без Go в PATH (7 бинарников, vault import/revoke, Broker MCP/auth, SQLite backup); Linux ARM64 семь бинарников запустились в изолированном контейнере без сети. Это локальные артефакты, не exact-CI и не подпись. +- Push CI `36868572492` и PR CI `36868579280` для точного RC9 SHA завершились до первого шага: GitHub annotation указывает failed account payments или spending limit. RC9 CI assets и подписанной provenance нет; public release остаётся no-go. OpenCode/DeepSeek disposable run `run-ab12be94116626bc5e2ed8d42889d7b1` завершился `succeeded`, patch пустой, cleanup done, vault owner null; модель в plan mode прямо сообщила, что не вызывала `touch`, поэтому tool-level denial ещё не принят. Второй build-mode/permissions-deny bootstrap подготовлен, но новая Virfield image operation снова держит `available=0`. Временный профиль удалён, обычный Broker `healthz=200`. + +### RC8 retry/backoff fix — 2026-10-01, 19:00 Бишкек + +- В OpenCode denied-write probe Virfield вернул `image_in_use` из-за отдельной image_build операции, которая при лимите 2 VM намеренно устанавливает `available=0` для всех новых leases. Run был отменен до получения VM; чужая загрузка не прерывалась. Обнаружен Broker bug: промежуточная выдача credential snapshot сбрасывала exponential backoff, поэтому issuer выполнял частые issue/abort циклы. Исправлено в dev `f937ccc`, добавлен regression test; полный `make check` прошёл. +- Live Broker image собран из точного RC8 commit и развернут только для Broker. Синтетический run `run-594708d118987f24a406c9410d3814ae` во время Virfield image lock показал retry_count 0 → 1 → 2, затем был отменён с `cleanup=done`; DeepSeek inference и Mattermost-сообщений не было. Evidence: `/Users/admin/Developer/execution-broker/state/private-publication-audit-20261001/rc8-live-backoff-probe.json`. +- Дополнительный Linux ARM64 архив RC8 точного SHA собран локально; все семь бинарников запустились в контейнере без сети, Go и provider credentials. Это не exact-CI артефакт. Docker Codex в credential-free диагностике прошёл дальше только при отключённых seccomp и AppArmor, затем упёрся в `RTM_NEWADDR: Operation not permitted`; production изоляция не менялась. +- Соседний Virfield image job `job-a56dffdf456ecf269dc7c18bce963a97` после полной загрузки IPSW и установки macOS перешёл в `needs_attention` на `assistant_dispatched` с `image_command_failed`. Общий maintenance lock остаётся, `available=0`; OpenCode denied-write VM ждёт восстановления владельцем Virfield job. +- Clean publication RC8 `6984bc67e4a9b991ae66acf06d91578ffcff586b` запушен в приватный PR. Push CI `36863848919` и PR CI `36863854456` не стартовали из-за GitHub Billing & plans, подтверждено annotation. Локальный Darwin ARM64 архив точного commit прошёл binary-only check-release с Go вне PATH и SHA256SUMS, но не является exact-CI release asset. Public stable no-go. +- Docker Engine 29.2.1 диагностирован отдельно: узкий seccomp профиль разрешает только user namespace и сохраняет запрет mount/network namespaces, но полный Codex bwrap sandbox всё ещё не запускается; даже одноразовый `seccomp=unconfined` дошёл до `Failed to make / slave: Permission denied`. Production worker flags не ослаблены. +- Подробный ledger: `/Users/admin/Developer/execution-broker/state/private-publication-audit-20261001/release-readiness-rc8.md`. + +### RC7 exact-SHA gate — 2026-10-01, 18:35 Бишкек + +- Clean publication commit `5c213bb98b0caaa24bc7bb07ba73bd8589bdd07a` запушен в существующий приватный PR #1. Его push CI `36862025838` и PR CI `36862030297` не выполнили ни одного шага: GitHub annotation сообщает failed account payments или недостаточный spending limit. Старый RC6 CI не является доказательством для RC7. Обновление Billing & plans необходимо до публикации exact-CI release assets. +- Локально в чистом checkout прошли `make check`, Callee race test, проверка 213 публичных файлов, plugin version `0.1.0-rc.7`. Локальный Darwin ARM64 RC7 архив из точного SHA прошёл `check-release.py` в чистом временном состоянии без Go в PATH, включая версии, vault, Broker MCP/auth и SQLite backup. Это локальный архив без подписи, не замена native CI artifacts. +- Подробный release ledger: `/Users/admin/Developer/execution-broker/state/private-publication-audit-20261001/release-readiness-rc7.md`. Public stable по-прежнему no-go. + +### OpenCode DeepSeek acceptance — 2026-10-01, 18:25 Бишкек + +- Существующий DeepSeek provider Balda использован без вывода ключа: ключ через private pipe импортирован в отдельный зашифрованный `opencode-deepseek-work` vault. Нативный OpenCode 1.18.34 вернул `OK` на реальном `deepseek/deepseek-flash` запросе. Credential-free VM bootstrap и постоянный issuer на `127.0.0.1:7796` установлены; рабочий профиль `macos-opencode-deepseek` подключен к Broker. +- Два полных синтетических Story (`run-cf83a0a3a0768d45f82199ae8db4176e`, `run-6b12f6189bc2e9b32fccadb596fc06b5`) прошли шесть ролей, Human approval, quality gate, шесть SHA-256-verified artifacts, Prism completion и cleanup. Второй запустился в новой VM и пережил перезапуск Broker/issuer на approval без browser login. +- Между ними был отказ `run-c6d2008abe6308000375c2a4e55c0c3e` только из-за добавленного моделью поля `notes` в валидный отчет verifier. Узкое исправление parser в dev `396eb5a` принимает `notes`, но всё ещё отвергает другие неизвестные поля и неисполненные/ошибочные проверки; Callee race и полный `make check` прошли. Для публикации нужен новый exact-SHA кандидат. +- OpenCode denied-write probe не дошёл до VM: Virfield зарезервирован соседней операцией image export (`image_in_use`). Проверочный run отменен с cleanup done, временный профиль удален. Upstream revocation/expiry ещё не проверены; static DeepSeek API key не имеет OAuth refresh. + +### RC6 candidate и актуальная приёмка — 2026-10-01, 17:25 Бишкек + +- Clean publication commit `2d786c4`, [CI 36854040694](https://github.com/mallexxx/execution-broker/actions/runs/36854040694) 14/14, приватный [draft RC6](https://github.com/mallexxx/execution-broker/releases/tag/untagged-4a40999515a9662c914c): четыре native CI-архива, plugin archive и SHA256SUMS побайтно сверены. GitHub draft SHA256 совпадают с локальным bundle. Remote main ещё не обновлён, публичного релиза нет. +- Claude full Story прошёл в двух свежих macOS VM, включая обычный профиль после restart issuer/Broker без browser login; reviewer/verifier/quality gate passed, cleanup done, Prism completed. Запрещённая запись в read-only роли отклонена. Managed plugin `0.1.0-rc.6` установлен в рабочую Balda из CI архива. +- Оставшиеся gates, доказательства и решение no-go: `/Users/admin/Developer/execution-broker/state/private-publication-audit-20261001/release-readiness-rc6.md`. До их закрытия RC6 остаётся приватным draft. + + +Базовый релиз Virfield v2.0.0 опубликован. На Mac установлен более новый versioned-images/GHCR кандидат, который ещё не принят как production release. Актуальные Git/runtime, матрица golden/clone E2E и полный оставшийся план поддерживаются только в [[personal/projects/virfield]]. Сервисы используют deployment вне checkout; публикация архива и коммит исходников не обновляют runtime автоматически. + +Локально подтверждено 2026-09-30: + +- отдельный `execution-broker` repo и sidecar deployment; доступ из реального Balda container через MCP; +- Virfield v2 lease → pinned SSH tunnel → Runner/Callee; чистый clone выполняет XCTest и отдаёт xcresult до cleanup; +- полный Codex workflow с шестью свежими ролями и Human approval; реальный локальный commit и пять unittest проходят; +- persistent OAuth vault/Keychain и host issuer: две последовательные свежие VM работают без нового browser login, checkpoint сохраняется до удаления VM; последняя проходит quality gate, restart Broker/issuer и Story completion; +- Prism/Beads projection и local Story lifecycle. Доказательства: `/Users/admin/Developer/execution-broker/state/private-publication-audit-20261001/acceptance.md`. + +До production/public stable release остаются Mattermost-origin/delivery, совместимый Docker Codex sandbox, приёмка credential lifecycle на Docker/remotes, реальные managed-host identities/TLS/rollback, согласованное восстановление всего стека, remote CI/clean-machine distribution/signing. Встроенные роли пока sequential; параллельные worktrees и модельные бюджеты не реализованы. Полный аудит: `/Users/admin/Developer/execution-broker/state/private-publication-audit-20261001/production-review.md`. + +Статус остальных проектов и provider-конфигурация проверяются в их собственных источниках; релиз Virfield не является подтверждением их готовности. + +Подтверждено 2026-10-01: + +- Чистый publication repo: `/Users/admin/Developer/execution-broker-public`, remote `https://github.com/mallexxx/execution-broker` (private), коммит `cb9d54b8369bfd7437b98ccf76b9ff3d2df397f5`. Частная development history не отправлена. Аудитные истории перенесены из public docs в private state. +- CI `36815717157`: 10/10 успешных заданий. Нативное исполнение Darwin/Linux ARM64/AMD64; Linux дополнительно устанавливается из архива без Go, собирает четыре образа, проходит real Callee/Beads Human, рестарт Broker/Prism, проверку пяти артефактов и cleanup. Пять именно CI-архивов и SHA256SUMS загружены в private draft `v0.1.0-rc.4`; подписи отсутствуют. +- Managed plugin `execution-broker` 0.1.0-rc.4 установлен через штатный Balda CLI. Старый standalone skill сохранён вне discovery roots. Файл Balda с MCP credentials имеет mode 0600; OAuth vault/Keychain сохранены. +- Balda PR 10 принят upstream. Исправление bounded user-visible preparation failure отправлено в PR 11; локальные race/lint/architecture проверки прошли. Рабочая Balda обновлена до `0.1.16-session-feedback`, подключение Mattermost подтверждено. Первый PR 11 CI выявил отдельную гонку старого restart-теста; исправлена в `e0e953bc`, 50 повторов под race detector прошли. Финальный PR 11 CI `36817575956`/`36817575952`: 8/8 успешных проверок. PR открыт; upstream merge ещё не выполнен. + +Весь стек нельзя объявлять полностью production-ready: свежий Mattermost-origin запуск нового managed `/execute` ещё не принят; Docker provider sandbox/автоматический OAuth на Docker и remotes, реальные host identities/TLS/rollback, per-role worktrees/model budgets и официальное подписанное распространение остаются отдельными незакрытыми границами. Имеющаяся приёмка Mattermost recovery и drained backup/restore не заменяет эти проверки. + + +### Дополнение 2026-10-01: Claude CLI и OpenCode + +В целевой объём включены Claude CLI и OpenCode с несколькими моделями. Balda выбирает allowlisted agent_profile из каталога Broker; executor/model задаются оператором. Секреты не входят в конфиг профиля, плагин, репозиторий или VM image. Callee остаётся владельцем workflow, а профиль не может ослабить authored permissions. + +Реализовано в ветке `codex/multi-executor-profiles`, clean publication commit `35cbe3ad5710f8eb5a52147da068ac016fb137c6`: + +- отдельные encrypted identities/Keychain accounts с backward compatibility существующего Codex vault; типизированные cache leases, проверка executor/provider/account и сохранение обновлённых credentials до освобождения VM; +- automated enrollment: Claude official setup-token через private PTY без печати токена; OpenCode auth login в отдельном HOME/XDG с импортом только выбранного provider; API-key ввод скрыт; браузерное согласие остаётся одноразовым действием человека; +- integrity-locked optional runtimes: Claude Code 2.1.286, Claude ACP 0.84.0, OpenCode 1.18.34; конфигурации и workflow packs, выбор модели, поддержка в каталоге и skill Balda; backup проверяет все identities; +- локально прошли race/vet/build, Callee, credential/enrollment/backup/publication checks; реальный credential-free CLI/ACP probe на macOS ARM64 прошёл, npm audit и полный clean-history Gitleaks без находок. CI на четырёх платформах: https://github.com/mallexxx/execution-broker/actions/runs/36820468335 — 14/14 успешных заданий; native optional executor probe прошёл на всех четырёх платформах. Draft PR: https://github.com/mallexxx/execution-broker/pull/1. + +Это ещё не принятые production-профили: нужны реальные Claude/OpenCode login/inference, повторная свежая VM без логина, expiration/revocation, проверка read-only режима и полный Balda Story. OpenCode OAuth без стабильного accountId отклоняется; Anthropic subscription OAuth не переносится в OpenCode. Автоматический issuer для Docker/remotes по-прежнему не реализован. RC4 и live Codex deployment не изменены. + +Документация: `/Users/admin/Developer/execution-broker/docs/executors.md`. Подготовлены private launchers `/Users/admin/Developer/execution-broker/state/executor-enrollment/enroll-claude.command` и `enroll-opencode.command`; пока не запускались и не обращались к действующим credentials. + +## 3. Владельцы ответственности + +### Balda — оркестратор верхнего уровня + +Balda: + +- принимает задачу и уточнения из Mattermost; +- создаёт и обновляет Story в Prism/Beads; +- выбирает разрешённый workflow по его идентификатору; +- запускает выполнение через Broker; +- показывает запросы Human и итог пользователю. + +Balda не управляет процессами внутри VM напрямую. Она работает только через контракты Prism adapter и Execution Broker. + +### Prism adapter + Beads — долговременное состояние + +Prism adapter принадлежит контуру Balda и физически может быть отдельным sidecar-контейнером. + +Он: + +- предоставляет Balda узкие операции Story/Task/phase; +- вызывает `bd`; +- подключается к Dolt/Beads database в соседнем контейнере; +- обеспечивает идемпотентную запись событий Broker; +- остаётся единственным писателем предметного состояния задачи. + +Broker, Callee и Codex не пишут в Beads напрямую. Они возвращают факты выполнения, а Balda решает, как отразить их в Story. + +### Execution Broker — инфраструктурный исполнитель + +Broker — отдельный проект и отдельный репозиторий `execution-broker`. В production он живёт отдельным sidecar-контейнером рядом с Balda. + +Он: + +- получает от Balda команду запуска; +- арендует VM через Virfield; +- запускает VM runner и Callee; +- хранит техническое состояние run и журнал событий; +- передаёт ввод, отмену и ссылки на артефакты. + +Broker не выбирает workflow, не исполняет его граф и не меняет Prism/Beads. + +### Virfield — граница управления хостом + +Virfield работает на Mac-хосте и владеет жизненным циклом VM: + +- clone/start/stop/delete; +- IP и SSH readiness; +- сетевые туннели; +- lease и принудительная уборка VM. + +Аутентифицированный HTTP API `/api/v1` и HTTP MCP `/mcp` реализованы; daemon слушает loopback. Для Broker остаются проверяемая сетевая доступность и integration contract. Произвольного host shell в API нет. + +### Callee и Codex — выполнение внутри VM + +Callee исполняет workflow: фазы, ветвления, циклы, Human-паузы и запуск ролей. + +`implementer`, `reviewer`, `verifier` — роли Callee. Каждая роль запускает новую Codex ACP-сессию с отдельным prompt и контекстом. + +Codex изменяет код, запускает `xcodebuild`, тесты и другие команды внутри VM. Codex не запускает Callee и не управляет Story. + +## 4. Prism и prism-callee + +Официальный `prism-callee` — плагин к coding-agent host. В штатной схеме он делает coding agent владельцем Beads и запускателем Callee: + +```text +Codex → prism-callee skill → Callee → дочерние Codex sessions +``` + +Эта штатная топология не является целевой, потому что верхним оркестратором должен оставаться Balda. + +В целевой схеме используются отдельно: + +- lifecycle и правила Prism — как основа skill/адаптера Balda; +- Callee workflow pack — как версионируемые workflow-файлы внутри VM; +- Beads — через отдельный Prism adapter; +- Codex — только как исполнитель ролей Callee. + +До реализации требуется проверить, какие части официального плагина можно импортировать без изменений, а какие нужно адаптировать для Balda. + +## 5. Прямой data flow + +1. Пользователь даёт Balda задачу в Mattermost без готового репозитория. +2. Balda читает `execution_profiles`, выбирает `workspace_enabled` Docker worker и вызывает `execution_repository_create` со стабильным idempotency key, текущими `chat_id/topic_id` и собственным коротким `name_hint`. +3. Broker через gateway создаёт локальный Git-репозиторий под настроенным workspace root и возвращает `workspace://` ID с полным commit SHA; Balda создаёт Prism Story. +4. Balda вызывает `execution_start` с этим ID/revision, `story_id`, теми же chat/topic и allowlisted workflow/agent profile. +5. Broker durable-записывает run, gateway монтирует только выбранный repo в одноразовый Linux worker, Runner запускает Callee и установленный ACP-исполнитель. Для Xcode вместо Docker используется отдельная VM Virfield. +6. События, Human response, артефакты и cleanup идут через Broker; Prism/Beads и Mattermost обновляет Balda. + +Пример полей запуска после создания репозитория: + +```json +{ + "idempotency_key": "story-42-attempt-1", + "story_id": "story-42", + "chat_id": "mattermost-chat-id", + "topic_id": "mattermost-topic-id", + "workflow_id": "opencode/story", + "workflow_version": "sha256:<точный digest установленного workflow>", + "repository": "workspace://broker-created-name", + "revision": "<полный Git commit SHA>", + "runtime_class": "linux-container", + "worker_profile": "docker-dev", + "agent_profile": "opencode-deepseek", + "task": "Добавить Go CLI и тесты", + "ttl_seconds": 3600 +} +``` + +Broker возвращает `run_id` после надёжной записи команды; выполнение продолжается асинхронно. + +## 6. Обратный data flow + +1. Codex возвращает результат роли в Callee по ACP. +2. Callee переводит workflow в следующее состояние и отдаёт структурированное событие VM runner. +3. VM runner передаёт событие Broker; Broker сохраняет его до ответа Balda. +4. Balda читает события через `execution_events(run_id, after_event_id)` и подтверждает курсор. +5. Balda обновляет Story через Prism adapter и публикует значимые изменения в Mattermost. + +События доставляются как минимум один раз. Повторная доставка безопасна благодаря уникальному `(run_id, event_id)`. + +Минимальное событие: + +```json +{ + "event_id": 17, + "run_id": "run-123", + "story_id": "bd-42", + "type": "role_completed", + "phase": "review", + "role": "reviewer", + "status": "passed", + "summary": "Tests pass", + "artifacts": [ + {"kind": "git_commit", "ref": "abc123"}, + {"kind": "xcresult", "ref": "artifact://run-123/tests"} + ] +} +``` + +Тяжёлые логи и `.xcresult` не помещаются в журнал. Broker хранит только ссылки, checksum, размер и срок жизни. + +## 7. Human approval + +1. Callee доходит до Human node и приостанавливает workflow. +2. VM runner отправляет `human_required` с вопросом и вариантами. +3. Broker фиксирует состояние `waiting_human`. +4. Balda показывает вопрос в Mattermost и получает ответ в том же session/topic. +5. Balda вызывает `execution_respond`; Broker доставляет ответ в конкретный paused run. + +Связка обязательна: `chat_id/topic_id ↔ story_id ↔ run_id ↔ human_request_id`. + +Ответ должен быть идемпотентным: повторный `human_request_id` не возобновляет workflow дважды. + +## 8. Execution Broker: контракт + +### Где живёт + +Отдельный контейнер в одном Docker network с Balda. У Broker собственный persistent volume с SQLite. + +Northbound: HTTP MCP для Balda. +Southbound: Docker gateway для `linux-container`, Virfield API и scoped SSH для `macos-vm`, отдельный host-runner для `remote-host`. +Внутри одноразового worker: Runner → Callee → выбранный ACP-исполнитель. + +### Основные MCP tools + +1. `execution_profiles` — workflow, worker и agent allowlist. +2. `execution_repository_create` — идемпотентно создать новый локальный Git repo для chat/topic. +3. `execution_start`, `execution_status`, `execution_events`, `execution_ack` — durable run и события. +4. `execution_respond`, `execution_cancel`, `execution_artifacts` — Human response, отмена и артефакты. +5. `execution_remote_plan` и отдельные Virfield image tools — управляемые host/VM сценарии. + +### Состояния run + +```text +accepted +→ provisioning +→ booting +→ running +→ waiting_human +→ running +→ succeeded | failed | canceled | expired +``` + +Каждый переход записывается транзакционно вместе с событием. После рестарта Broker продолжает незавершённые run или переводит их в явно диагностируемое состояние. + +### Внутреннее состояние Broker + +Broker хранит только технические данные: + +- `run_id`, `story_id`, idempotency key; +- VM id, IP, lease deadline; +- workflow id/version и process/session id; +- event cursor, terminal status и error code; +- artifact references и cleanup status. + +### VM runner + +Нужен небольшой процесс внутри образа VM. Он отделяет Broker от консольного формата Callee. + +Runner: + +- проверяет входной manifest и allowlist workflow; +- запускает Callee в рабочей директории репозитория; +- преобразует lifecycle/output в стабильные JSON events; +- принимает cancel и Human response; +- возвращает terminal result и artifact manifest. + +Broker не должен парсить человекочитаемый ANSI/TTY-вывод Callee. Если Callee уже имеет стабильный machine-readable API, runner становится тонким адаптером. + +## 9. Execution workers и распределение ресурсов + +Broker планирует не только macOS VM. Все среды исполняют один versioned Runner protocol, но имеют разные `runtime_class`: + +- `linux-container` — обычный код, CLI, Go build/test и статическая проверка Dockerfile; Docker socket в worker не передаётся, фактическая сборка образа требует отдельного host/builder шага; +- `macos-vm` — Xcode, macOS APIs, GUI и UI tests; +- `remote-host` — изменение конкретного хоста после Human approval. + +Минимальный WorkerSpec: + +```json +{ + "runtime_class": "macos-vm", + "worker_profile": "xcode-27", + "agent_profile": "codex-default", + "workflow_id": "prism/story", + "repository": "git@host:org/repo.git", + "revision": "main" +} +``` + +### Параллельные задачи + +Balda поддерживает несколько независимых chat sessions. Turns внутри одной session сериализуются, разные sessions могут работать параллельно. + +Callee поддерживает `Parallel`, но это fan-out Role visits внутри одного workflow. VM создаёт и распределяет Broker, не Balda и не Callee. + +Политика пула: + +1. Одна изменяющая macOS Story получает одну эксклюзивную VM. +2. Implementer, reviewer и verifier используют ту же VM, но свежие ACP-сессии. +3. Отдельная VM на каждый Role не создаётся. +4. Одновременно работают максимум две macOS VM; остальные run стоят в очереди. +5. Linux containers масштабируются отдельно и не занимают macOS VM slots. + +Параллельные read-only роли могут работать в одной VM. Параллельные изменения получают отдельные Git worktrees; запись нескольких агентов в один checkout запрещена политикой Runner. + +Ограничение в две macOS VM задаёт Apple Virtualization.framework, а не Balda. Практический лимит дополнительно зависит от RAM и CPU. + +## 10. Изоляция, shared folder и секреты + +Для `macos-vm` основная граница — одноразовая VM: исходники и Xcode build живут на её локальном диске, без host shared folders. Virfield выдаёт scoped lease/SSH, а VM удаляется после выгрузки результатов. + +Для `linux-container` граница — одноразовый worker с read-only root, UID 65532, ограниченными capabilities и отдельным `/state`. Gateway имеет Docker socket и mount настроенного workspace root; сам worker получает только bind mount одного проверенного локального Git repo в `/workspace`. Broker и Balda не получают этот host mount. Запуск с `workspace://` требует `workspace_enabled`, соответствия chat/topic, точного commit SHA и чистого checkout. Credential issuer выдаёт scoped lease только после durable admission, checkpoint и cleanup завершают владение. Docker CLI/socket внутри worker отсутствуют. + +Ни Balda, ни модель не получают пользовательский `~/.ssh`, credential vault или произвольный host shell. + +### Credential management — обязательный scope (2026-09-30) + +OAuth-вход выполняется один раз при enrollment или при необходимости повторной авторизации у провайдера. Перезапуск или пересоздание VM сам по себе не должен требовать нового входа. + +Владелец provider credentials — отдельный host credential issuer Execution Broker. Virfield управляет VM/SSH leases, но не хранит OAuth пользователя. Balda, Prism/Beads и Callee-события получают только состояние; токены в их сообщения не передаются. + +Обязательные свойства: + +- долговременное зашифрованное хранилище вне VM и Git; на macOS master key находится в Keychain; права 0700/0600, atomic write и fsync; +- выдача в конкретный run, repository и agent profile после durable admission; приватный файл в HOME гостя, не в checkout, environment, argv или golden/bootstrap; +- одна OAuth identity арендуется одним run одновременно, чтобы VM не конкурировали за refresh token; ownership переживает restart Broker/issuer; +- перед release VM обновлённый cache сохраняется обратно через приватный канал только после остановки ролей; блокировка снимается после подтверждённого удаления VM; +- local revoke, provider-side revoke как отдельная операция, безопасное восстановление после утраты VM, отсутствие токенов в публичных API/артефактах; TTL Broker не считается upstream revocation. + +Приёмка: новый clone выполняет реальные Codex роли без браузерного login; Broker/issuer переживают restart; checkpoint сохраняется до cleanup; следующий clone использует сохранённую сессию. Отдельно тестируются конфликт владельца, повреждение vault, отказ refresh, revoke и невозможность восстановить отозванный токен поздним checkpoint. Потерянный checkpoint блокирует повторную выдачу до явного восстановления, а не молча использует старый token. + +Реализация и границы текущей версии: `/Users/admin/Developer/execution-broker/docs/credentials.md`. Live macOS приёмка завершена: `run-f94853e932ef9b620a28412b609c62c7`, все 6 ролей и quality gate пройдены, 5 unittest, cleanup done, encrypted checkpoint generation 6. Активный профиль Balda — `macos-codex` + `prism/story`; enrollment-only профиль отключён. Поддержка автоматического issuer сначала включена для macOS VM; Docker/remotes требуют отдельной приёмки того же жизненного цикла. OAuth может требовать нового входа после provider revoke/refresh rejection; это не штатный шаг каждого запуска VM. + +### Workspace и артефакты + +Текущий recipe Virfield создаёт VM без shared host folders. Код, mutable workspace и Xcode build живут на локальном диске VM; перенос исходников/результатов — через scoped SSH и Git либо `rsync/scp`. Артефакты выгружаются до release lease и получают уникальный путь `run_id/...`. + +Добавление host mounts потребует отдельного решения по изоляции и проверки согласованности данных; это не часть принятого recipe. + +### Изменение других хостов + +Задачи настройки Mac, Kraken, TrueNAS и других машин используют `runtime_class: remote-host`. + +1. Agent в контейнере формирует plan и выполняет read-only preflight. +2. Balda показывает точный diff действий и запрашивает Human approval. +3. Broker выдаёт capability lease на конкретные host, identity, operations и TTL 15–30 минут. +4. Runner применяет изменения через dedicated service account или установленный host-runner. +5. Verify, audit log и rollback закрывают lease. + +Личные ключи из пользовательского `~/.ssh` не передаются. Для managed host используется небольшой Go host-runner. Для первичного bootstrap допускается отдельный SSH account с short-lived certificate и ограниченным `sudo`. + +Изменения самого Mac выполняет отдельный native launchd helper. Он принимает подписанный capability lease; Balda и модель не получают постоянный host shell. + +## 11. Неподтверждённое + +### Блокирует прототип + +1. Есть ли у Callee стабильный machine-readable event stream и программный resume Human node. +2. Можно ли запустить workflow pack `prism/story` отдельно от stock plugin host. +3. Как Codex ACP получает workspace/cwd и сохраняет его между Role visits. +4. Как передавать cancel в Callee без повреждения terminal result. +5. Как подключить Broker к реализованному Virfield API через TLS/routing и проверить lease/SSH contract из его окружения. + +### Требует проверки до production + +- credential-free установка Codex; отдельное долговременное credential management с выдачей в каждую disposable VM (авторизация в golden image запрещена); +- доступность Dolt/Beads из Prism adapter после рестартов; +- формат и размер Xcode артефактов; +- восстановление Broker после падения во время `waiting_human`; +- параллельные Story и лимиты VM/CPU/RAM на Mac. + +## 12. Варианты + +### A. Sidecar Broker + VM runner — выбран + +Balda остаётся оркестратором, Broker изолирует инфраструктуру, Callee владеет workflow, Codex работает в VM. Это целевая схема. + +### B. Broker как host daemon + +Меньше сетевых переходов к Virfield и SSH, но Broker получает более широкую поверхность хоста. Использовать только если контейнерный sidecar не может надёжно управлять VM. + +### C. Stock prism-callee plugin + +`Codex → plugin → Callee → Codex`. Быстрее для проверки Prism, но Balda перестаёт быть владельцем lifecycle. Подходит как временный reference test, не как production topology. + +### D. Native remote ACP + +Будущая transport abstraction в `runtime + acpagent` позволит Balda или Callee подключаться к ACP по WebSocket/Streamable HTTP. Она может заменить SSH-транспорт, но не заменяет Broker, VM lifecycle, durable events и cleanup. + +## 13. Virfield v2 — реализовано и опубликовано + +Релиз [v2.0.0](https://github.com/mallexxx/virfield/releases/tag/v2.0.0) опубликован 2026-09-30, commit `ee3aa1d`. Текущая карточка — [[personal/projects/virfield]]; команды и контракты принадлежат документации репозитория. + +Реализованы единый Go control plane, SQLite journal, reconciliation, leases/idempotency, resource admission, image pipeline, scoped SSH, cleanup, CLI/MCP и embedded console. Лимит двух VM и неизвестные состояния обрабатываются явно; неопределённая мутация не повторяется автоматически. Personal SSH keys/PAT storage и client-side lifecycle fallback в текущей реализации отсутствуют. + +Исторический релиз v2.0.0 использовал Lume 0.5.3; его CI и golden/clone acceptance относятся к release commit 2026-09-30. С 2026-10-08 установленный Virfield использует Tart + SSH nodes. В hardening-кандидате 2026-10-14 Lume backend/factory/wrapper/build/install code удалён; старый отдельный Lume LaunchDaemon отключён и выгружен 2026-10-14 после проверки пустого inventory. Новый кандидат ещё не принят для релиза; текущие Tart/factory live результаты записаны отдельно. Текущие evidence и gates — в [[personal/projects/virfield]] и [Verification](https://github.com/mallexxx/virfield/blob/codex/retire-lume-release-hardening/docs/VERIFICATION.md). + +### Граница проектов и репозиториев — принятое решение + +`virfield` и `execution-broker` — два независимых проекта. Их нельзя объединять в один репозиторий или один daemon. + +Репозиторий `virfield` содержит host control plane: корневой Go module, `cmd/virfieldd`, `cmd/virfield`, `cmd/virfield-mcp`, реализации в `internal/`, deployment в `deploy/`. Актуальная карта модулей — [Architecture](https://github.com/mallexxx/virfield/blob/codex/retire-lume-release-hardening/docs/ARCHITECTURE.md); отдельного каталога приложения v2 нет. + +Целевая структура отдельного `execution-broker` (реализацию сверять в его репозитории): + +```text +cmd/broker +cmd/runner +cmd/host-runner +internal/scheduler +internal/execution +internal/events +internal/drivers/virfield +internal/drivers/container +internal/drivers/remotehost +api/ +``` + +Границы ответственности: + +1. `virfield` управляет VM, images, leases и host-level cleanup; он ничего не знает о Callee, Prism, Beads и model providers. +2. `execution-broker` выбирает execution class, запускает Runner/Callee, управляет agent profiles и хранит run event log; он не управляет Lume напрямую. +3. `balda` вызывает Broker по MCP и отражает события в Prism/Beads; Virfield напрямую из Balda не вызывается. +4. `execution-broker → virfieldd` использует versioned authenticated HTTP API `/api/v1`; текущие модели и маршруты — `internal/domain`, `internal/httpapi`, `internal/client`. Отдельная OpenAPI-спецификация Virfield в v2.0.0 не опубликована. +5. Общих Go-пакетов между репозиториями нет; совместимость обеспечивается API contract и contract tests. + +## 14. Agent profiles и модельная политика + +Broker и Runner не содержат vendor-specific workflow logic. Run выбирает `agent_profile` из статического allowlist. + +Для Callee напрямую подтверждены Codex, Claude, Copilot, Grok, OpenCode и Cursor. Gemini и GLM подключаются через `generic_acp` или отдельный ACP adapter и считаются неподтверждёнными до integration test. + +Worker image содержит Runner, Callee и нужные provider binaries. Credentials выбранного provider выдаются только на срок run. Node отсутствует в Virfield/Broker control plane; он допускается внутри специализированного worker image, если конкретный agent CLI без него не работает. + +### Распределение моделей + +Схема `Balda → Callee → coding agent` остаётся правильной. Provider для Balda и профили исполнителей настраиваются в соответствующих проектах; конкретное назначение не фиксируется в плане Virfield. + +Рекомендуемая раскладка: + +- Balda — надёжное понимание запроса, выбор workflow, управление state и Human interaction; +- specify/design/review — сильная reasoning-модель; +- implementation — Codex или другой сильный coding agent; +- build/test/SSH apply — детерминированные Script/Runner operations; +- дешёвая модель — routing, summarization и простая диагностика. + +DeepSeek подходит для ограниченного routing и простых операций с жёсткими tool schemas. Он не принимает самостоятельные решения о privileged SSH-командах и сложной архитектуре. + +Оптимизация стоимости строится на deterministic routing, token/run budgets, bounded retries и escalation: после повторной неудачи задача повышает model tier, а не повторяет дешёвую модель бесконечно. + +## 15. Pitfalls + +### Архитектура + +1. Смешивание двух топологий: stock `Codex → Callee` и целевой `Balda → Broker → Callee`. +2. Перенос workflow-логики в Broker превращает его во второй оркестратор. +3. Прямая запись Broker/Callee/Codex в Beads создаёт несколько владельцев состояния. +4. Запуск Callee вне VM переносит Script nodes и часть команд на хост. +5. Парсинг консольного вывода Callee вместо стабильного event protocol ломает resume и диагностику. + +### Надёжность + +1. Потеря события между Callee и Balda без durable event log. +2. Двойной запуск VM без idempotency key. +3. Повторный Human response возобновляет workflow дважды. +4. Падение Broker оставляет VM без lease и cleanup. +5. Большие build-логи забивают SQLite или MCP-ответы. + +### Безопасность + +1. Непроверенный `workflow_id`, repository URL или revision превращается в выполнение произвольного ввода. +2. Общий SSH/Git credential между VM расширяет последствия компрометации. +3. Секреты в prompt, events или build logs попадают в Mattermost/Beads. +4. Host path, переданный как ACP cwd, не существует или указывает не туда внутри VM. +5. VM с доступом в локальную сеть может атаковать хостовые сервисы; egress и host API нужно ограничить. + +### Исполнение и ресурсы + +1. Mutable build через virtiofs может прочитать stale content; workspace должен жить на локальном диске worker. +2. Более двух активных macOS VM невозможны; Broker обязан иметь очередь и admission control. +3. Parallel Callee roles, пишущие в один checkout, создают file conflicts даже внутри одной VM. +4. Свободный provider command или agent-generated `cmd` превращает профиль в arbitrary code execution. +5. Установка всех provider credentials в один golden image нарушает isolation и усложняет rotation. + +## 16. План работ после push `f237556` + +Предыдущие этапы Broker/Runner, Virfield v2 и credential lifecycle выполнены частично и описаны выше как история. Текущий критический путь относится к новому локальному repo и Docker/OpenCode: + +1. **Свести исходники и принять точный build.** Ветка `codex/broker-workspace-repos` (`f237556`) основана на RC9, поэтому соединить её с Docker/OpenCode runtime-исправлениями `5792cff`; зафиксировать plugin version, workflow digest, Runner/Callee и immutable worker image ID. Проверить `make check` и повторить изолированный Story на этом exact commit. +2. **Развернуть live контур.** Настроить выделенный Docker worker profile, `workspace_root=/Users/admin/Developer`, mount корня только в gateway и `--workspace-repositories` у issuer; установить новую версию Broker/gateway и managed plugin Balda. Проверить MCP discovery и отсутствие регрессии действующих VM профилей. +3. **Принять пользовательский Mattermost flow.** Сообщение Balda не содержит repo name/path. Она сама вызывает `execution_repository_create`, создаёт Prism Story, запускает run, доставляет Human approval и события в исходный chat/topic, публикует артефакты и фиксирует cleanup. Проверить Git commit в новом repo и отсутствие двойного создания при повторе idempotency key. +4. **Закрыть release gates.** Разобрать наблюдённый ACP `fs/write_text_file: Method not found`, выполнить CI/publication checks на точном SHA, проверить rollback и оставшиеся credential/managed-host ограничения. Public stable объявлять только после live приёмки. + +### Дополнительные задачи — 2026-10-11: альтернативные аппрувы и ACP-контекст + +- [x] **Telegram для Broker workflows и worker ACP.** Реализованы привязка к run/Human/владельцу, выбор единственного канала запроса, возврат результата в исходную беседу, срок действия, обновление кнопок, restart recovery и competing-channel deduplication. Текущий контракт и доказательства: [[personal/projects/balda/execution-broker]]. +- [ ] **Прямые ACP-разрешения хостовых агентов Balda.** Единая межканальная доставка этих разрешений не покрывается Telegram-механизмом Broker; это отдельная незавершённая часть плана. +- [ ] **Прогон субагента для автоаппрува и сбора дополнительного контекста команд ACP-субагентов.** Перед решением по запросу разрешения запускать отдельного субагента: собрать цель команды, cwd, аргументы, затрагиваемые файлы/ресурсы, ожидаемые побочные эффекты и связь с текущей задачей. Использовать собранный контекст для автоаппрува в пределах настроенной политики; при недостатке данных или необходимости решения пользователя направлять запрос на ручной аппрув, включая Telegram. Сохранять решение и обоснование в событиях run. +- [ ] **Приёмочный прогон.** Проверить связку ACP-запрос → сбор контекста субагентом → автоаппрув либо ручной ответ через Telegram → возобновление нужного ACP-субагента. Покрыть недостаточный контекст, отказ, таймаут и повторный ответ. + +## 17. Критерии готовности + +1. Balda сама создаёт новый локальный Git repo под настроенным workspace root из обычной Mattermost-задачи; пользователь не передаёт заранее repo name/path. +2. Один chat/topic получает одну Prism Story и идемпотентный Broker run с полным pinned commit SHA, allowlisted workflow и agent profile. +3. Docker worker запускает установленный ACP-исполнитель без Docker socket и host secrets; Xcode/macOS задачи остаются в VM Virfield. +4. Human approval возвращается в исходную тему, события и артефакты доходят до Balda/Prism/Mattermost, terminal cleanup и credential checkpoint завершаются. +5. Повторный запрос/рестарт не создаёт второй repo или run; отказ, cancel и rollback дают диагностируемое состояние. +6. CI и release доказательства относятся к одному exact SHA объединённого кода; remote-host задачи требуют отдельный capability lease. + +## Связанные заметки + +- [[personal/projects/virfield]] — текущий релиз, подтверждённый scope и каноническая документация. + +- [[personal/tech/callee-prism]] — контракты Callee, ACP и Prism. diff --git a/personal/projects/archive/balda-mattermost-connector-design-2026-09.md b/personal/projects/archive/balda-mattermost-connector-design-2026-09.md new file mode 100644 index 00000000..d99e33bb --- /dev/null +++ b/personal/projects/archive/balda-mattermost-connector-design-2026-09.md @@ -0,0 +1,810 @@ +# Balda — Mattermost-коннектор: исторический дизайн + +> [!WARNING] Историческая спецификация +> Код Mattermost-коннектора принят в [PR #8](https://github.com/baldaworks/balda/pull/8), merged **2026-09-29**; статус перепроверен **2026-10-14**. +> Схема, пути, команды и результаты ниже описывают версию сентября и не служат текущим runbook. +> Текущий запуск: [[personal/projects/balda/setup]]. Текущие PR и код: [[personal/projects/balda/balda]]. Broker/ingress recovery и приёмка: [[personal/projects/balda/execution-broker]]. + +> Создано 2026-09-26. Обновлено 2026-09-28 (правки по ревью применены). +> Статус PR: **MERGED, 2026-09-29**. Приведённые ниже CI и commit IDs — история приёмки PR #8. +> 🟢 **PR:** https://github.com/baldaworks/balda/pull/8 — 46 файлов, +6850/−27, 9 коммитов. +> Фаза 2 закрыта полностью: код, 103+ новых теста, доки, зелёный CI. +> Блокер `tools/` снят: `/tools/` в `.gitignore` + `git rm -r --cached tools/`. +> Детали — [[personal/plans/balda-mm-pr-plan]]. +> 🔧 **2026-09-28 — правки по ревью `metalagman`:** голова `05342c5a`, CI 8/8 PASS. +> Закрыты slash-команды (новый HTTP-приёмник `command_server.go`), `/locator` +> (`locatorref` + guard-тест), групповой DM (сессия по чату), и **`ScopeResolver`-регистрация, +> которой не было в PR**. Разбор — [[personal/plans/balda-mm-pr8-review-fixes]]. +> Часть проекта [[personal/projects/balda/balda-mm-delegation-plan]] (Фаза 2). +> Требование Alex: поддержка **всех** функций Балды, по новым API, production-качество. +> +> **Проверено (2026-09-26, PR):** +> - `go build ./...` → exit 0 · `go vet ./...` → exit 0 +> - `go tool golangci-lint run` → **0 issues** +> - `go tool go-arch-lint check --project-path .` → **OK - No warnings found** +> - **DI-граф собирается целиком** с `balda.mattermost.enabled=true` — проверено +> `validateBaldaApplication(prepared)` через fx, а не только компиляцией +> - **103 новых теста** зелёные, включая `-race`; в CI — 126 PASS, 0 FAIL, 0 SKIP +> - **CI: 8/8 джобов PASS** на чистом раннере (`.github/workflows`) +> +> 🔴 **Исторический урок (Фаза 3):** «все quality gates пройдены» оказалось неполным — +> при первом **живом запуске** на Kraken Балда ушла в crash-loop: +> `advertised commands have no actor handler: goal`. В `Advertisement.Names` была объявлена +> несуществующая команда `goal` (это payload kind, а не имя хендлера; цикл называется +> `goalkeeper`). **Ни `go build`, ни `go vet`, ни `golangci-lint`, ни `go-arch-lint`, ни +> `validateBaldaApplication` этого не увидели.** Исправлено в `mattermostfx/module.go` и +> `inbound.go` дельтой `tools/deltas-goal-command-fix.json`. Разбор — +> [[personal/tech/balda-go-fx-pitfalls]] §«Дефект 4». +> 📌 Вывод: «Фаза 2 закрыта» означала «код компилируется и граф собирается», но **не** +> «транспорт работает». Живой запуск обязателен. Статус Фазы 3 — +> [[personal/projects/balda/balda-kraken-mm-deploy]]. +> +## ✅ PR-ПРОХОД 2026-09-26 — транспорт приведён к PR-качеству + +Ветка `feat/mattermost-channel`, **7 коммитов**, PR **открыт** — baldaworks/balda#8. +Подробности и полный список — [[personal/plans/balda-mm-pr-plan]]. + +``` +9f1e6c9c chore: ignore local project-editing tooling ← уборка tools/ из PR +58f27120 docs(mattermost): document the transport setup and behavior +80d4f41d test(mattermost): cover the transport, the ingress and the handler +c7f18654 fix(mattermost): send a monotonic seq on every client frame +6dc3077a feat(mattermost): add the inbound handler and wire it into DI +fa1a2855 feat(auth): support the Mattermost channel subject +004fda2f feat(mattermost): add Mattermost transport adapter ← база прошлой сессии +``` + +**Исправлено в коде (не в данных):** + +| Дефект | Фикс | +|---|---| +| 🔴 Двойной префикс `principal` — прошлый фикс был в БД и вернулся | `auth.MattermostSubject()` теперь **идемпотентен** (срезает `mattermost:` перед повторной квалификацией); из `handler.go` убран `TrimPrefix`-костыль | +| `ProgressPolicy.Typing: true` в command-пути | Приведено к `false` — у ботов MM нет typing API, `SendTyping` осознанный no-op | + +**Тесты — 6 новых файлов, 103 теста:** `locator_codec_test.go` (15), `inbound_test.go` (18), +`adapter_test.go` (17), `client_test.go` (13), `ingress_test.go` (23) + +`handlersfx/mattermost_ingress_test.go` (17). Ключевые регрессионные guard'ы: +`writeEvent` всегда с `seq` и уникальный `seq` под 16 горутинами (`bad_seq`), +plain-fallback только на content-rejection (не на 5xx), идемпотентность subject'а, +тред = одна сессия ≠ канальной, `/start` доступен до авторизации. +Пакеты `mattermost` + `handlersfx` в verbose-прогоне: **126 PASS, 0 FAIL, 0 SKIP**. + +**Доки:** README + `docs/balda.md` + новый **`docs/mattermost.md`** (создание бота, +минтинг токена, membership, 5 ключей, поведение, troubleshooting). + +**Все гейты зелёные на закоммиченном дереве:** +``` +go build ./... → 0 +go vet ./... → 0 +go tool golangci-lint run → 0 issues +go tool go-arch-lint check --project-path . → OK - No warnings found +go test -race (затронутые пакеты) → все ok +``` +⚠️ 5 пакетов падают локально (`cmd/balda`, `apps/balda`, `mcpruntime`, `session`, `state`) — +**это апстримная база**, проверено `git stash` (те же 4 теста падают без правок). +🔴 **В CI эти же тесты ПРОШЛИ** — локальное падение было особенностью машины, не дефектом. + +**`tools/` вычищен из ветки:** `/tools/` в `.gitignore` + `git rm -r --cached tools/` +(коммит `9f1e6c9c`). Файлы на диске целы (39 шт.), в PR-diff их нет. +⚠️ `gitignore` **не отвязывает** уже отслеживаемые файлы — нужны оба шага. + +--- + +## Статус Фазы 2: код закрыт (исторический раздел ниже) + +> Разделы ниже — сначала контракт/разведка, затем **фактически написанные файлы**. Ищи +> §«Что фактически написано» для реестра файлов. +> +> 🔧 **Инструмент правок:** `tools/apply-project-deltas.py` + `tools/run-project-deltas.sh` + +> `tools/deltas-mattermost*.json` — правки проекта идут **текстовыми дельтами с якорями**, +> не sed/in-place. См. §«Метод правок: дельта-апплаер». + +--- + +## Где работаем + +| | | +|---|---| +| Репозиторий | `~/Developer/balda` (форк `mallexxx/balda`) | +| Ветка | **`feat/mattermost-channel`** — создана от **чистого `upstream/main`** | +| База | `de273d8f feat(balda): embed backoffice in startup lifecycle` | +| Апстрим | `baldaworks/balda` (`normahq/balda` редиректит туда — SHA идентичны) | +| Наши коммиты | **не тащим** (отставание 426 коммитов; решение в плане) | + +Проверка: `git rev-list --left-right --count main...upstream/main` → `2 426`. + +⚠️ В форке остаётся `5ad65955` — `replace normahq/norma => ./norma-local` в `go.mod`. +На ветке от upstream его нет (ветка чистая), но при любом мерже с нашей `main` — удалять. + +--- + +## Контракт: `docs/architecture/transport-presentation-boundary.md` + +Ключевое правило (generic invariant для транспорта `X`): +- `X` может зависеть от shared-контрактов; shared-контракты **не могут** зависеть от `X`; +- внешний wiring импортирует `X` (root) и `Xfx`, **но не** внутренние `presentation`/`delivery`; +- всё UX-специфичное живёт **внутри** `X`. + +Два пути вывода: +1. **Model-authored text** → prompt-format путь. Транспорт может инжектить свои правила форматирования. +2. **System-authored service messages** → типизированные дескрипторы + детерминированные рендереры, + регистрируемые каждым транспортом отдельно. **Не** в shared-пакетах. + +--- + +## Образец: `internal/apps/balda/channel/zulip/` + +Размеры (эталон объёма для MM): + +| Файл | Строк | +|---|---| +| `adapter.go` | 331 | +| `adapter_test.go` | ~235 | +| `client.go` | 289 | +| `server.go` | 316 | +| `server_test.go` | 593 | +| `locator_codec.go` | 229 | +| `inbound.go` | 192 | +| `formatting.go` | 14 | +| `presentation/locator.go` | 18 | +| `presentation/goal.go` | 76 | +| `zulipfx/{module,prompt,structured}.go` | 66 + 36 + 54 | + +### `zulipfx/module.go` — что регистрируется через DI + +```go +var Module = fx.Module("balda_channel_zulip_fx", + fx.Provide( + // 1. Advertisement — список поддерживаемых команд для транспорта + fx.Annotate(func(enabled bool) commandcmd.Advertisement { + return commandcmd.Advertisement{Transport: zulip.ChannelType, Enabled: enabled, + Names: []string{"locator","reset","usage","auto","cancel","goalkeeper","topic","close","start","user","skill"}} + }, fx.ParamTags(`name:"balda_zulip_webhook_enabled"`), + fx.ResultTags(`group:"balda_command_advertisements"`)), + + zulip.NewServer, // 2. ingress carrier + func(client *zulip.Client, logger zerolog.Logger) *zulip.Adapter { + adapter := zulip.NewAdapter(client, logger) + adapter.SetTypingThrottleInterval(4 * time.Second) + return adapter + }, // 3. delivery adapter + + fx.Annotate(func(a *zulip.Adapter) deliveryfx.ChannelAdapterBinding { + return deliveryfx.ChannelAdapterBinding{ChannelType: zulip.ChannelType, Adapter: a} + }, fx.ResultTags(`group:"balda_delivery_channel_adapter"`)), // 4. binding в роутер + + fx.Annotate(NewPromptRegistryContribution, + fx.ResultTags(`group:"balda_delivery_prompt_contribution"`)), // 5. prompt-правила + + fx.Annotate(NewLocatorStructuredRegistrar, // 6. structured renderers + fx.ResultTags(`group:"balda_delivery_structured_registrar"`)), + fx.Annotate(NewGoalProgressStructuredRegistrar, …), + fx.Annotate(NewGoalOutcomeStructuredRegistrar, …), + + fx.Annotate(func(h *zulip.Server) appports.TransportLifecycleStage { + return appports.TransportLifecycleStage{Name: "zulip ingress", Start: h.Start, Stop: h.Stop} + }, fx.ResultTags(`group:"balda_transport_lifecycle_stage"`)), // 7. lifecycle + ), +) +``` + +**7 групп DI — столько же нужно для MM.** Имена групп фиксированы. + +### `locator_codec.go` — модель локатора + +- `ChannelType = "zulip"` (MM: `"mattermost"`), `sessionIDPrefix = "zu"` (MM: `"mm"`). +- Address payload — JSON: `{type, stream_id, topic, user_id}`. MM: `{type, channel_id, root_id, user_id}`. +- AddressKey: `s::` / `dm:`. MM: `c:` / `dm:` / `t:`. +- SessionID: `zu-s--<8 hex от sha256(topic)>` / `zu-dm-`. +- Три обязательные функции: `NewXLMLocator`, `DecodeLocator` (возвращает `(addr, ok, err)`), + `LocatorFromAddressKey`, `ClassifyLocatorScope` → `LocatorScopePersonal` / `LocatorScopeGroup`. +- Дополнительно: `UserID(int) string` ↔ `ParseUserID(string) (int, error)` — префикс `zu-`/`mm-`. + +### `adapter.go` — `deliverycmd.Adapter` + +Реализует `var _ deliverycmd.Adapter = (*Adapter)(nil)`. Константы операций +(`deliverycmd.Operation*`), которые надо покрыть: `Plain`, `Markdown`, `AgentReply`, +`Draft`, `Typing`, `Progress`. Результат — `deliverycmd.Result{ProviderMessageID string}`. + +Обязательные публичные методы (по образцу Zulip — вызываются роутером и gateway): +`SendPlain`, `SendMarkdown`, `SendMarkdownWithFormat`, `SendAgentReply`, +`SendAgentReplyWithProviderMessageID`, `SendAgentReplyWithProviderMessageIDAndFormat`, +`SendDraftPlain`, `SendTyping`, `SendProgress`, `Deliver`. + +**Питфоллы, заложенные в Zulip-адаптере (повторить):** +- `sendWithPlainFallback` — если провайдер отбил markdown (`isContentRejectedError`), + отправить plain-вариант. Ошибки **объединять** через `errors.Join`, не терять первую. +- Fallback **только** на content-rejection (400/BAD_REQUEST), **не** на 5xx — иначе + дубли сообщений при временных сбоях. +- Typing **throttle** по `locator.SessionID` (Zulip — 4 с), с инъектируемым `now func() time.Time` + (нужно для тестов). Поле `typingLastSentAt map[string]time.Time` под mutex. +- `validateReady()` — явная проверка `client != nil`, возвращает человекочитаемую ошибку. +- `SendDraftPlain` для Zulip — **no-op** (`return nil`), т.к. нет edit-in-place. + Для **MM это не no-op**: у MM есть `PUT /posts/{id}` (edit) — реализовать по-настоящему. +- `SendProgress`: `ProgressThinking` → no-op; `ProgressPlanUpdate` → отправить текст. + +### `inbound.go` — нормализация входа + +Возвращает `turncmd.NormalizedInbound` с полями: `ID` (`turncmd.InboundID("zulip:"+msgID)`), +`Text`, `Locator`, `ProviderMessageID`, `UserID`, `MessageID`, `ReceivedAt` (RFC3339 UTC), +`DeliveryFormat`, `ProgressPolicy{Typing, PlanUpdates}`, `Direct`, `Source`. + +MM: `Source` = новый `turncmd.SourceMattermost`, `DeliveryFormat` = `DeliveryFormatMarkdown`, +`ProgressPolicy{Typing: false, PlanUpdates: true}` — **в MM нет typing-индикатора в каналах** +(есть только в DM через WebSocket `user_typing`; для каналов не работает). + +Также в `inbound.go`: `InboundMessage`, `InboundCommand`, интерфейс `InboundProcessor` +(3 метода: `ProcessInbound`, `HandleCommand`, `HandleUnsupportedCommand`), +`SupportedCommands() []string`, `commandSupported(name)`. + +### `ingress.go` — ingress (фактически реализовано) + +⚠️ **Zulip использует outgoing webhook по HTTP.** У MM для ботов — **WebSocket** +(`wss:///api/v4/websocket`). Это отличается от Zulip-образца принципиально. + +**Как реализована аутентификация (проверено компиляцией):** +- dial с заголовком `Authorization: Bearer `; +- **плюс** обязательный первый кадр `{"seq":1,"action":"authentication_challenge","data":{"token":""}}` — + без него MM не начинает слать события; +- `WebSocketURL()` в `client.go` сам конвертирует `http→ws` / `https→wss` и добавляет `/api/v4/websocket`. + +⚠️ В ранних заметках было `Sec-WebSocket-Protocol: bearer ` — **это не то, что реализовано +и не то, что описано в актуальных доках MM.** Использовать `authentication_challenge`. + +Перенесено из Zulip-сервера и работает: +- **семафор на конкурентную обработку** (`processSem chan struct{}`, 16) + `processWG`; + при заполнении — дроп с warn-логом, не накопление; +- **`defer recover()`** вокруг обработки поста → не ронять процесс; +- **фильтр эхо-сообщений** (`IsBotEcho`) → нет бесконечного цикла; +- **`SetReadLimit(1 MiB)`** на WS-кадр; +- **reconnect-loop** с backoff 5 с — `readLoop` переустанавливает соединение, пока контекст жив; +- **отдельная reader-горутина**: блокирующий `ReadMessage` не прерывается контекстом, поэтому + `Stop()` закрывает conn, и горутина выходит по ошибке; +- graceful `Stop()` с ожиданием активной обработки (`waitForProcessing`); +- `Start`/`Stop` в `appports.TransportLifecycleStage`. + +**Специфика MM, которой не было в Zulip:** +- В **канале** Балда отвечает только на `@mention` (`MentionsBot`/`StripMention`); в DM — на всё. +- События `post_edited` / `post_deleted` **осознанно игнорируются**: Балда действует по исходной + инструкции и не переписывает свой ответ задним числом. +- Системные посты (`Type != ""`) и удалённые отсеиваются до семафора. +- `data.post` в событии `posted` — это **JSON-строка**, а не объект (нужен двойной `Unmarshal`). +- Каналы типа `D` / `G` — это DM/групповой чат, а не канал команды. + +### `presentation/` + `formatting.go` + +- `formatting.go`: `FormattingPromptRuleAndExample() (rule, example string)` — + правило для модели (MM-совместимый Markdown), и `MarkdownPlainText(text)`. +- `presentation/locator.go`: `RenderLocator(body locatorfmt.Response) string` — локатор + YAML-блок + `target: locator` / `key: ` для scheduler/webhook. +- `presentation/goal.go`: `RenderGoalProgress(goalkeepercmd.GoalProgress)` и + `RenderGoalOutcome(goalkeepercmd.GoalOutcome)` — используют `ShouldRenderValidation()`, + `ShouldRenderVerified()`, `ShouldRenderNotVerified()`, `ShouldRenderNextAction()`. + +### `zulipfx/{prompt,structured}.go` + +- `prompt.go` → `deliveryfx.PromptRegistryContribution` с тремя полями: + `Formats` (правило+пример), `Formatters` (реализация `deliveryfmt.Formatter`: + `Name() Name` + `Format(text) (Message, error)`), `Routes` + (`{Transport, DeliveryFormat, RegisteredName}`). +- `structured.go` → по одному `deliveryfx.StructuredRegistryRegistrar` на каждый message kind + через `deliveryfx.NewStructuredRegistrar(deliveryfmt.TransportX, , renderer)`. + Renderer реализует `RenderStructured(ctx, deliveryfmt.StructuredEnvelope[T]) (StructuredPresentation, error)`. + Три штуки: `locatorfmt.ResponseDescriptor`, `goalkeepercmd.ProgressDescriptor`, + `goalkeepercmd.OutcomeDescriptor`. + +--- + +## Что нужно добавить в shared-слой — ✅ ВСЁ ПРИМЕНЕНО + +| Файл | Добавить | Статус | +|---|---|---| +| `deliverycmd/channel.go` | `ChannelTypeMattermost ChannelType = "mattermost"` | ✅ | +| `deliveryfmt/routes.go` | `TransportMattermost = "mattermost"`, `NameMattermostMarkdown Name = "mattermost_markdown"` | ✅ | +| `turncmd` (source) | `SourceMattermost` | ✅ | +| `docs/commands.md` | колонка Mattermost в таблице доступности команд | ✅ | +| `docs/reference/configuration.md` | 5 ключей `balda.mattermost.*` | ✅ | +| `README.md` / `docs/balda.md` | ⚠️ **не обновлялись** — README всё ещё перечисляет 3 chat surface | ⏳ | + +⚠️ `channel/router.go` **править не нужно** — он map-based по `locator.ChannelType`. +Достаточно зарегистрировать binding в DI (группа `balda_delivery_channel_adapter`). + +📌 Остаётся один незакрытый пункт требований `AGENTS.md` (не блокирующий): README и +`docs/balda.md` не упоминают Mattermost. Закрыть при подготовке PR в апстрим либо в Фазе 3. + +--- + +## Что фактически написано (2026-09-26) + +Всё в `~/Developer/balda` на ветке **`feat/mattermost-channel`**. `go build ./...` = exit 0. + +### Новый пакет `internal/apps/balda/channel/mattermost/` + +| Файл | Строк | Содержимое | +|---|---|---| +| `client.go` | ~560 | REST v4: `CreatePost`, `UpdatePost`, `GetPost`, `GetChannel`, `GetUser`, `GetMe`, `GetTeamByName`, `GetChannelByName`, `GetDirectChannel`, `UploadFile`, `CreatePostWithFiles`, `AddChannelMember`, `Ping`; `WebSocketURL()`, `APIBaseURL()`, `ValidateConfig()`; `APIError` | +| `locator_codec.go` | ~250 | `ChannelType="mattermost"`, префикс `mm`. AddressKey: **`c:` / `c::` / `d:`**. `NewChannelLocator(teamID, channelID, rootID)`, `NewDMLocator(channelID, userID)`, `DecodeLocator`, `LocatorFromAddressKey`, `ClassifyLocatorScope`, `ChannelIDFromLocator`, `RootIDFromLocator`, `IsDirectLocator`, `UserID`/`ParseUserID` | +| `inbound.go` | ~250 | `InboundMessage`/`InboundCommand`/`InboundProcessor` (3 метода), `NormalizeInbound`, `LocatorForPost`, `IsDirectChannelType` (`D`/`G`), `IsBotEcho`, `IsSystemPost`, `IsDeletedPost`, `StripMention`, `MentionsBot`, `ParseCommand`, `SupportedCommands`, `WebSocketEvent`/`PostedData` | +| `adapter.go` | ~420 | Все **9** `deliverycmd.Operation`: Plain, Markdown, AgentReply, Draft, Typing, Progress, **ClearQuestionControls, Photo, Document**. `sendWithPlainFallback`, `MarkdownPlainText`, `sendMedia` | +| `media.go` | ~45 | `openMediaFile` (лимит 100 МБ), `mediaBaseName` | +| `ingress.go` | ~380 | **WebSocket ingress**: dial с `Authorization: Bearer`, `authentication_challenge`, reconnect-loop, ping 30 с, `processSem` (16), `processWG`, `defer recover()` | +| `formatting.go` | ~14 | `FormattingPromptRuleAndExample()` | +| `presentation/presentation.go` | ~110 | `RenderLocator`, `RenderGoalProgress`, `RenderGoalOutcome` | +| `mattermostfx/module.go` | ~100 | 7 групп DI (как `zulipfx`) | +| `mattermostfx/config.go` | ~25 | `NewClient` + валидация при `enabled` | +| `mattermostfx/ingress.go` | ~40 | `IngressParams` (`fx.In`), `NewIngress` | +| `mattermostfx/structured.go` | ~100 | prompt contribution + 3 structured registrar | + +### Изменения в shared-слое (все применены) + +| Файл | Что добавлено | +|---|---| +| `deliverycmd/channel.go` | `ChannelTypeMattermost ChannelType = "mattermost"` | +| `deliveryfmt/routes.go` | `TransportMattermost = "mattermost"`, `NameMattermostMarkdown Name = "mattermost_markdown"`, маршрут `{TransportMattermost, DeliveryFormatNone, NamePlainText}` в `BuiltinRoutes()` | +| `turncmd/turn.go` | `SourceMattermost = "mattermost"` | +| `internal/apps/balda/app.go` | `mattermostfx.Module` в список модулей (строка ~667) | + +### Wiring-дельты (2026-09-26, вечер — закрыт блокер сборки) + +Это то, что оставалось «незакрытым» в первой половине дня. Применено дельта-апплаером. + +| Файл | Что добавлено | Зачем | +|---|---|---| +| `internal/apps/balda/app.go` | импорты `channel/mattermost` **и** `channel/mattermost/mattermostfx` | корневой пакет нужен для `mattermost.ValidateConfig`; `mattermostfx` — для `Module` | +| `internal/apps/balda/app.go` | **5 fx-провайдеров** зарегистрированных строк: `balda_mattermost_enabled`, `_server_url`, `_token`, `_bot_user_id`, `_bot_username` | модуль запрашивает их через `name:`-теги — без провайдеров fx падает «missing types» | +| `internal/apps/balda/app.go` | `validateMattermostConfig(cfg MattermostConfig) error` + вызов рядом с `validateSlackConfig` | fail-fast при `enabled=true` без URL/токена/bot_user_id | +| `internal/apps/balda/app.go` | функция вставлена **перед** `validateIdentifierValue` | порядок как у соседей | +| `internal/apps/balda/config.go` | поле `Mattermost MattermostConfig` в `BaldaConfig` + тип `MattermostConfig{Enabled, ServerURL, Token, BotUserID, BotUsername}` | конфиг-поверхность | +| `cmd/balda/command.go` | `!cfg.Balda.Mattermost.Enabled` в `validateBaldaChannelConfiguration` + подсказка про `BALDA_MATTERMOST_ENABLED` в тексте ошибки | иначе MM-only деплой отбивается «at least one channel is required» | +| `cmd/balda/balda.yaml` | дефолтный блок `balda.mattermost` с 5 ключами и комментариями | шаблон генератора | +| `docs/reference/configuration.md` | 5 строк описания `balda.mattermost.*` (перед `balda.slack.bot_token`) | документация ключей | +| `docs/commands.md` | абзац про поведение MM (mention в каналах, `/slash`-команды, нет typing) + колонка `Mattermost` в таблице + строка `/plugin ...` | таблица доступности команд | + +⚠️ **`app.go` правился в 4 захода** (импорт → провайдер enabled → валидация → остальные 4 строки). +Каждый заход — отдельная дельта-группа; всё в `tools/deltas-mattermost*.json`. + +### 🔴 Три реальных дефекта, найденных тестами (не компиляцией) + +Компиляция проходила, но fx-граф был сломан. Поймано только запуском +`validateBaldaApplication` с включённым Mattermost. + +**1. `fx.In` нельзя аннотировать внешним `fx.ParamTags`** — дефект в `mattermostfx/module.go`. + +Симптом: +``` +Failed: invalid annotation function func(mattermostfx.IngressParams) (*mattermost.Ingress, error): + fx.In structs cannot be annotated with fx.ParamTags or fx.From +``` + +Причина: в `module.go` `NewIngress` был обёрнут в `fx.Annotate(NewIngress, fx.ParamTags(...))`. +Но `IngressParams` **уже** содержит `fx.In` — его теги обязаны жить **внутри** структуры. + +**Правильно (как в `zulipfx`/`zulip.ServerParams`):** +```go +// ingress.go — теги на полях структуры +type IngressParams struct { + fx.In + Processor mattermost.InboundProcessor `optional:"true"` + Client *mattermost.Client + Commands *commandcmd.Registry + Enabled bool `name:"balda_mattermost_enabled"` + BotUserID string `name:"balda_mattermost_bot_user_id"` + BotUsername string `name:"balda_mattermost_bot_username"` + Logger zerolog.Logger +} + +// module.go — голое предоставление, БЕЗ fx.Annotate/fx.ParamTags +fx.Provide(NewIngress), +``` +⚠️ `Commands` был `*commandcmd.Registry `optional:"true"`` — заменён на обязательный +`*commandcmd.Registry` (как у Zulip). `commandcmd.Registry` в графе есть всегда. + +**2. Отсутствовали провайдеры зарегистрированных строк.** + +Симптом: `missing types: string[name="balda_mattermost_server_url"] (did you mean to Provide it?)` +×3 (server_url, token, bot_user_id). + +Причина: модуль читает значения через `fx.ParamTags`/`name:`, а провайдеров этих имён в +`app.go` не было. У Zulip они есть (`balda_zulip_*`) — для MM забыли. + +**Правильно:** 5 `fx.Provide(fx.Annotate(func() ..., fx.ResultTags(`name:"balda_mattermost_*"`)))`. +Для несуществующих ключей YAML `strings.TrimSpace` даёт `""` — безопасно, как у Zulip. +**Каждый новый `name:`-тег в модуле = обязательный провайдер в `app.go`.** + +**3. Отсутствовал импорт корневого пакета `mattermost`.** +`app.go:965: undefined: mattermost` — `validateMattermostConfig` вызывает +`mattermost.ValidateConfig`, а импортирован был только `mattermostfx`. + +### Тесты (написаны 2026-09-26) + +| Тест | Файл | Что проверяет | +|---|---|---| +| `TestLoadConfigDocument_AppliesMattermostEnvOverrides` | `cmd/balda/command_test.go` | 5 env-override `BALDA_MATTERMOST_*` доезжают в `doc.Balda.Mattermost` | +| `TestValidateCommandRunsValidationWithMattermostTransport` | `cmd/balda/command_test.go` | **реальный** `loadBaldaCommandConfig(true)` + `validateBaldaApplication(prepared)` — весь fx-граф с включённым MM | +| — (встроено в существующие) | — | регрессионная проверка `deliveryfmt` | + +### 🔴 Тесты транспорта и хендлера (2026-09-26, сессия PR-подготовки) + +Шесть новых файлов, ~90 тестов, все зелёные включая `-race`. + +| Файл | Кол-во | Что покрывает | +|---|---|---| +| `channel/mattermost/locator_codec_test.go` | ~16 | локаторы channel/thread/DM; **изоляция thread↔channel сессий**; `LocatorFromAddressKey`; `ClassifyLocatorScope`; `UserID`/`ParseUserID`; отказ чужим транспортам | +| `channel/mattermost/inbound_test.go` | ~17 | `NormalizeInbound` → shared-контракт; `Typing:false`; `LocatorForPost` (канал/тред/DM); фильтры; `ParseCommand`; whitelist (`goalkeeper` есть, `goal` нет); `SupportedCommands` возвращает копию | +| `channel/mattermost/adapter_test.go` | ~16 | все `Deliver`-операции; plain-fallback **только** на 400; draft-as-post; typing = no-op **без сети**; thread `root_id`; 5xx **не** ретраится | +| `channel/mattermost/client_test.go` | ~13 | ws/wss derivation; `Authorization: Bearer`; `APIError`; `isContentRejectedError`; контекст; 2xx без `id` = ошибка | +| `channel/mattermost/ingress_test.go` | ~20 | 🔴 **регрессия `bad_seq`**; монотонность/уникальность `seq` под 16 горутинами; все фильтры; `recover()`; семафор; `Start` отказ без `bot_user_id` | +| `handlersfx/mattermost_ingress_test.go` | ~18 | публикация всех команд; 🔴 **одиночный префикс principal**; `/start` до авторизации; отказ чужим без публикации; nil-safety | + +**Метод (для будущих транспортов):** + +- **REST** — `httptest.NewServer` + fake-handler на `POST /api/v4/posts`, запоминающий + запросы и с инъецируемым `respond(attempt, req) (status, body)`. Так проверяются + fallback-сценарии (первый запрос → 400, второй → 201). +- **WebSocket** — реальный `websocket.Upgrader` в `httptest` + `websocket.DefaultDialer`. + Единственный способ проверить, что `writeEvent` всегда шлёт `seq`. +- **Хендлер** — тестируется **структура напрямую** (`&mattermostInboundHandler{...}`), + без fx: так проверяются авторизация, префикс principal и nil-safety в изоляции. +- `fake...OwnerKVStore` + `recording...CommandIngress` — по образцу `zulip_ingress_test.go`. + +### 🔴 Дефект P1.1 — двойной префикс `principal`: починен в КОДЕ + +**Симптом (прод):** `/start owner=` в DM отвечает успехом, но `@balda` даёт +`settlement_stage="unauthorized"` навсегда. + +**Корень:** `auth.MattermostSubject(userID)` добавляет префикс +(`fmt.Sprintf("%s:%s", ChannelMattermost, ...)`), а `actors/command/start/handler.go:420` +вызывал её поверх `p.Principal`, который **уже** приходит префиксованным от +`mattermost_ingress.go:147` (`Principal: auth.MattermostSubject(cmd.SenderID)`). +Результат в БД — `mattermost:mattermost:`, а `GetUserByBinding` сравнивает **точно** → +совпадений нет → `unauthorized`. Плюс мусор в `display_name` и `created_by` channel-токенов. + +**Прошлый «фикс» был в данных** (прямой `UPDATE` в `state.db`) — и дефект **вернулся** при +следующей записи. 🔴 **Правило: дефект, найденный в данных, чинить в коде.** + +**Фикс (дельта `tools/deltas-mattermost-23-prefix-idempotent.json`):** хелпер сделан +**идемпотентным** — срезает уже существующий префикс: + +```go +// MattermostSubject returns the channel-qualified subject for a Mattermost user. +// It is idempotent: a value that is already channel-qualified ("mattermost:") +// yields the same subject, so callers may pass either a raw user ID or a subject +// without risking a doubled prefix ("mattermost:mattermost:"). +func MattermostSubject(userID string) string { + trimmed := strings.TrimSpace(userID) + trimmed = strings.TrimPrefix(trimmed, ChannelMattermost+":") + return fmt.Sprintf("%s:%s", ChannelMattermost, trimmed) +} +``` + +Из `userSubject` убран локальный `TrimPrefix`-костыль: +```go +case transportMattermost: + return auth.MattermostSubject(trimmed) +``` + +📌 **Почему фикс в хелпере, а не в вызывающем коде:** `MattermostSubject` вызывается из +трёх мест (`userSubject`, `mattermost_ingress.go:147`, `authorizeInbound`). Идемпотентность +закрывает **все** пути сразу и защищает от будущих вызовов поверх готового subject'а. +**Остальные вызовы были корректны** — дефект ровно один, но источник общий. + +**Регресс-тесты:** `TestMattermostSubjectIsIdempotent` (3 случая: raw, subject, padded) и +`TestMattermostHandlerSendsChannelQualifiedPrincipalExactlyOnce` (через публикуемую команду). + +### 🔴 Дефект P1.6 — `ProgressPolicy.Typing` расходился между путями + +`handlersfx/mattermost_ingress.go` в `HandleCommand` ставил +`ProgressPolicy{Typing: true, PlanUpdates: true}`, тогда как inbound-путь +(`inbound.go:135`) и `Adapter.SendTyping` (осознанный no-op — у ботов MM нет typing API) +говорили `false`. Мёртвый код: `SendProgress` при `Typing:true` вызывал `SendTyping`, +который всегда возвращал `nil`, и на каждом ходу писался бы warn только при ошибке. + +**Фикс** (дельта `tools/deltas-mattermost-24-typing.json`): `Typing: false` — command-путь +приведён к inbound-пути. **Оба пути теперь согласованы, и тест это фиксирует.** + +📌 **Урок:** при копировании Zulip-паттерна проверять каждую фичу на применимость к +провайдеру. Pan-путь и turn-путь обязаны давать **одинаковую** `ProgressPolicy` для одного +транспорта — расхождение невидимо компилятору. + +### 🔴 Второй урок: не доверять «проверил — зелёно» без проверки в изоляции + +Три моих первых теста падали, и **все три — моя ошибка, не дефект кода**: + +| Тест | Что я предположил | Реальность | +|---|---|---| +| `TestLocatorForPostScopesReplyToThreadRoot` | тред-сессия == сессия root-поста | Тред **отдельная** сессия от канала (`mm-t-...` vs `mm-c-...`) — это **правильный** дизайн, тест переписан на проверку «два ответа в треде → одна сессия» | +| `TestNewIngressFallsBackToClientUserID` | `NewIngress` подставляет bot_user_id из клиента | Не подставляет. Гард живёт в `onStart` («refuses to start without bot user id») — тест переписан на реальный контракт | +| `TestMattermostHandlerRejectsUnauthorizedCommand` | отказ возвращает `nil` | Возвращает `runtime is unavailable` (нет dispatcher для доставки отказа). **Главное — команда НЕ публикуется**, это и проверяется | + +📌 **Дыра в дизайне, найденная тестом:** root-пост треда (`RootID=""`) → сессия канала +`mm-c-`, а ответ в этом треде (`RootID=post-1`) → `mm-t-`. +То есть тред — **отдельная** беседа от канала. Это соответствует комментарию в +`locator_codec.go` («the locator key must stay stable for a whole thread»), но означает: +**бот, отвечающий в тред, ведёт отдельную от канала сессию.** Зафиксировано тестом +`TestLocatorForPostScopesReplyToThreadRoot` (два ответа в треде → одна сессия). + +⚠️ **Метод:** когда тест падает — сначала перечитать код и понять, чей это контракт. +Из трёх падений **ноль** оказались дефектами кода; все — неверные предположения теста. + + +🔴 **Ключевой урок:** `go build ./...` = exit 0 **не значит**, что транспорт рабочий. +fx-граф проверяется только `validateBaldaApplication` (или `fx.ValidateApp`). Компиляция +не видит ни `fx.In`-несовместимость, ни отсутствующие провайдеры. + +**Как воспроизвести проверку графа (эталон):** +```go +prepared, err := loadBaldaCommandConfig(true) // нужен os.Chdir в tempdir с .config/balda/config.yaml +err = validateBaldaApplication(prepared) // ← вот это и есть проверка графа +``` +Конфиг для теста — `mattermost.enabled: true` + `server_url`/`token`/`bot_user_id`/`bot_username`, +провайдер `balda_agent: opencode_acp`. Якорь `func TestValidateCommandRunsValidation(t *testing.T) {` +использован как точка вставки. + +### ✅ Текущее состояние — ЗАКРЫТО (2026-09-26, ночь) + +Оба ранее открытых пункта выполнены: + +1. ✅ **`TestBuiltinRoutes`** — в map `want` добавлен маршрут Mattermost + (`internal/apps/balda/deliveryfmt/registry_test.go`). Пакет `deliveryfmt` зелёный. +2. ✅ **`golangci-lint`** — запущен, 4 замечания → исправлены (см. §«Что дал golangci-lint» ниже). + +Дополнительно в этом же заходе: +- ✅ **`.go-arch-lint.yml` обновлён** — компоненты `mattermost_transport` / `mattermost_wiring`, + права зависимости у `composition_root` и `ingress_wiring`. Требование `AGENTS.md`. +- ✅ **Удалён мёртвый typing-throttle** целиком (не заглушен). +- ✅ Закоммичено: `004fda2f feat(mattermost): add Mattermost transport adapter` (40 файлов, +3072). + +**Ничего не осталось по коду.** Дальше — Фаза 3 (развернуть Балду с MM на Kraken, +провайдер DeepSeek) → [[personal/projects/balda/balda-mm-delegation-plan]]. + +### Что дал `golangci-lint` (4 замечания, все закрыты) + +| Замечание | Файл | Фикс | +|---|---|---| +| `inamedparam` ×2 | `ingress.go:452-453` (interface assertion) | `Start(ctx context.Context) error` / `Stop(ctx context.Context) error` | +| `unused` | `adapter.go:149` `shouldSendTyping` | удалён вместе со всем throttle-механизмом | +| `unused` | `ingress.go:21` `websocketReceiveTimeout` | константа удалена | +| `unlambda` | `mattermostfx/module.go:51` обёртка `func(client, logger) *Adapter` | заменена на голое `mattermost.NewAdapter` | + +**Корневая причина:** typing-throttle (`shouldSendTyping`, `typingThrottleInterval`, +`typingLastSentAt`, `typingMu`, `SetTypingThrottleInterval`) был скопирован из Zulip +по аналогии, но у MM-ботов **нет typing API** — `SendTyping` всегда `return nil`, значит +весь механизм недостижим. Удалён **целиком группой** (каскад: поле → setter → `sync` → +`zerolog` в `module.go`), а не заглушен `nolint`. + +📌 **Урок для будущих транспортов:** при копировании Zulip-адаптера проверять каждую фичу +на применимость к провайдеру. Перенос «по аналогии» создаёт мёртвый код, который линтер +находит только на финальном прогоне. Детали — [[personal/tech/balda-go-fx-pitfalls]]. + +### `.go-arch-lint.yml` — что добавлено + +Требование `AGENTS.md`: архитектурное изменение обязано обновлять arch-lint в том же коммите. +Без этого `go tool go-arch-lint check` падает двумя классами ошибок +(`composition_root shouldn't depend on …/mattermost`, `not attached to any component in archfile`). + +4 правки по образцу `zulip`: +- `components:` → `mattermost_transport` (`channel/mattermost` + `presentation/`) и + `mattermost_wiring` (`mattermostfx/`) +- `mayDependOn` для обоих новых компонентов +- `composition_root` → `+ mattermost_transport, mattermost_wiring` +- `ingress_wiring` → `+ mattermost_transport` + +Полные YAML-фрагменты — [[personal/tech/balda-go-fx-pitfalls]] §«go-arch-lint». + +### ✅ Финальные quality gates — все зелёные + +``` +go build ./... → exit 0 +go vet ./... → exit 0 +go tool golangci-lint run → 0 issues +go tool go-arch-lint check --project-path . → OK - No warnings found +тесты Mattermost → 3 PASS +``` + +### ✅ Предсуществующие падения тестов — НЕ наши + +13 пакетов падают **и на чистом `upstream/main`**. Проверено `git stash` (правки убраны → +тесты перезапущены → правки возвращены). Падающие: +`TestInitCommand_NonInteractiveAutoSelectsRootAndGeneratesDetectedAgents` (cmd/balda), +`TestMigrateSessionMemoryLayout_*` ×4 (state), `TestWorkspaceMCPImport/Export_*` (session), +`TestPluginResolverResolvesContainedArgvFilteredEnvAndLaunchSecrets` (mcpruntime), +`TestResolveWorkspaceBaseBranch_ConfigPreferredWhenValid`, +`TestCanonicalSessionMemory*` (apps/balda). +⚠️ Не пытаться «чинить» их как часть MM-задачи — это дефекты апстрима, отдельная работа. + +### Отклонения от плана разведки (важно) + +1. **`server.go` → `ingress.go`.** Планировался `server.go` по образцу Zulip, но у MM нет + HTTP-вебхука — это долгоживущее WS-соединение, а не `http.Server`. Имя `ingress.go` + точнее отражает суть. +2. **`SendDraftPlain` — не no-op.** В плане было «для MM это не no-op, edit-in-place есть». + По факту: `OperationDraft` **отправляется как обычный пост** (текст не теряется), а + edit-in-place живёт в `OperationClearQuestionControls` → `PUT /posts/{id}` — там он + семантически уместен (гашение кнопок после выбора). +3. **AddressKey не `t:`.** Планировалось `c:` / `dm:` / `t:`. По факту тред кодируется + **тем же channel-ключом с суффиксом**: `c:` (канал) и `c::` (тред). Причина: + тред MM — это root-post внутри канала, и локатор должен оставаться стабильным для всего треда. +4. **`ProgressPolicy{Typing: false}`.** Подтверждено: у MM-ботов **нет typing API** + (`SendTyping` возвращает `nil` осознанно — иначе шум на каждом ходу). +5. **`gorilla/websocket v1.5.3` был `// indirect`** в `go.mod` — стал прямым при сборке. +6. **`commandSupport` переименован** в экспортируемый `IngressCommandSupport` (нужен для `fx.In`), + оставлен алиас. + +### ✅ Импорт mattermostfx — ЗАКРЫТО + +`internal/apps/balda/app.go` — импорты добавлены (строки 27-28, рядом с `zulipfx`): +```go +"github.com/baldaworks/balda/internal/apps/balda/channel/mattermost" +"github.com/baldaworks/balda/internal/apps/balda/channel/mattermost/mattermostfx" +``` +Корневой `mattermost` нужен для `mattermost.ValidateConfig` в `validateMattermostConfig`. +`mattermostfx.Module` в списке модулей (строка ~667). `go build ./...` = exit 0. + +### 🔴 Что понадобилось дополнительно (не было в исходном коде) + +Первоначально модуль компилировался, но **DI-граф не собирался**. Три дефекта: + +1. **`fx.In` нельзя аннотировать внешним `fx.ParamTags`.** `IngressParams` содержит `fx.In`, + а в `module.go` стоял `fx.Annotate(NewIngress, fx.ParamTags(...))` → fx падает с + `fx.In structs cannot be annotated with fx.ParamTags or fx.From`. + **Фикс:** `name:`-теги перенесены **в поля `IngressParams`**, `NewIngress` без `fx.Annotate`. + Так же сделано в `zulipfx` (`zulip.NewServer` идёт голой ссылкой, теги в `ServerParams`). +2. **Не хватало провайдеров registered strings.** Модуль запрашивает + `balda_mattermost_{server_url,token,bot_user_id,bot_username}` — их никто не предоставлял. + **Фикс:** 4 `fx.Provide` в `app.go` (блок `// Mattermost transport`), значения берутся из + `cfg.Balda.Mattermost.*` через `strings.TrimSpace`. Для несуществующих ключей + `GetString` возвращает `""` — тот же паттерн, что у `balda_zulip_*`. +3. **Валидация стояла не там.** Функцию `validateMattermostConfig` добавили в `app.go` + (рядом с `validateSlackConfig`), вызов — после `validateSlackConfig` в `Module()`. + +**Метод, который это поймал:** `go build`/`go vet` этих ошибок **не видят** — только реальный +`fx.ValidateApp` через `validateBaldaApplication`. Для любого нового транспорта писать такой +тест сразу: он ловит неполный wiring, а не только синтаксис. + +### Конфиг, который нужен для запуска + +Ключи читаются через `fx.ParamTags` / `name:"…"`: + +``` +balda_mattermost_enabled +balda_mattermost_server_url +balda_mattermost_token +balda_mattermost_bot_user_id +balda_mattermost_bot_username +``` +Соответствие в `config.yaml` (по образцу `balda.zulip.*`): `balda.mattermost.{enabled,server_url,token,bot_user_id,bot_username}`. +Конкретные имена ключей yaml **проверить в `docs/reference/configuration.md`** перед Фазой 3 — +я их не сверял, только зафиксировал DI-теги. + +✅ **Проверено 2026-09-26:** env-имена — `BALDA_MATTERMOST_ENABLED`, `BALDA_MATTERMOST_SERVER_URL`, +`BALDA_MATTERMOST_TOKEN`, `BALDA_MATTERMOST_BOT_USER_ID`, `BALDA_MATTERMOST_BOT_USERNAME`. +Механизм: `appconfig/loader.go:367` делает `SetEnvPrefix("BALDA")` + +`SetEnvKeyReplacer(".", "_")` на leaf-ключах. **Env применяется только к ключам, которые уже есть +в YAML** — поэтому блок `balda.mattermost` обязан присутствовать в `cmd/balda/balda.yaml`, +иначе env-переменные молча игнорируются. Это же верно для любого нового транспорта. + +### Метод правок: дельта-апплаер (инструмент проекта) + +Правки проекта идут **не sed/не in-place**, а текстовыми дельтами. Причина: правило Alex +«никогда не использовать in-place скрипты и sed; писать скрипт, потом исполнять» + нужна +идемпотентность при повторных заходах. + +| Файл | Роль | +|---|---| +| `tools/apply-project-deltas.py` | движок: применяет JSON-дельты, ведёт состояние | +| `tools/run-project-deltas.sh` | обёртка: `check` / `apply` / `list` | +| `tools/deltas-mattermost.json` | основная группа (12 дельт: wiring + доки) | +| `tools/deltas-mattermost-2.json` | импорт корневого `mattermost` | +| `tools/deltas-mattermost-3.json` | тест env-override | +| `tools/deltas-mattermost-4.json` | тест fx-графа | +| `tools/deltas-mattermost-5.json` | фикс `fx.In` + `module.go` | +| `tools/deltas-mattermost-6.json` | 4 провайдера строк | +| `tools/deltas-mattermost-7.json` | маршрут Mattermost в `TestBuiltinRoutes` | +| `tools/deltas-mattermost-8.json` | `.go-arch-lint.yml`: `mattermost_transport`/`mattermost_wiring` | +| `tools/deltas-mattermost-9.json` | `inamedparam` + удаление `shouldSendTyping`/полей/`websocketReceiveTimeout` | +| `tools/deltas-mattermost-10.json` | удаление `SetTypingThrottleInterval` + `typingMu` + вызов/импорт `time` в `module.go` | +| `tools/deltas-mattermost-11.json` | удаление неиспользуемого импорта `sync` | +| `tools/deltas-mattermost-12.json` | `unlambda`: голое `mattermost.NewAdapter` | +| `tools/deltas-mattermost-13.json` | удаление неиспользуемого импорта `zerolog` из `module.go` | +| `tools/deltas-mattermost-23-prefix-idempotent.json` | 🔴 **P1.1:** `MattermostSubject` идемпотентна (срезает существующий префикс) + убран `TrimPrefix`-костыль из `userSubject` | +| `tools/deltas-mattermost-24-typing.json` | 🔴 **P1.6:** `ProgressPolicy.Typing` → `false` в command-пути (`handlersfx/mattermost_ingress.go`) | +| `tools/deltas-mattermost-25-docs-balda.json` | `docs/balda.md`: Mattermost в runtime stack, typing-нюанс, ссылка на гайд | +| `tools/deltas-mattermost-26-docs-readme.json` | `README.md`: Mattermost в 4 списках + `balda.mattermost.*` + ссылка на гайд | +| `tools/.deltas-state.json` | состояние (sha256 применённых якорей) | + +📌 **Дельты 9-13 — это один каскад линтера.** Удаление throttle-механизма тянуло +`SetTypingThrottleInterval` → `typingMu` → `sync` → `zerolog`. Пришлось делать 5 заходов, +каждый раз пересобирая. **Урок: удалять мёртвую фичу одной группой дельт**, а не по одному +симптому — тогда каскад импортов закрывается за раз. + +**Формат дельты:** +```json +{ "file": "путь/относительно/корня", "note": "что делаем", + "search": "уникальный фрагмент текущего текста", + "replace": "чем заменить" } +``` + +**Три состояния, которые различает апплаер:** +- `replace` уже есть в файле → `skipped` (идемпотентно, повторный прогон безопасен) +- `search` есть ровно один раз → `applied` +- ни `search`, ни `replace` → `error: ЯКОРЬ НЕ НАЙДЕН` (файл изменился не так, как ожидалось) + +**Команды:** +```bash +cd ~/Developer/balda +bash tools/run-project-deltas.sh check tools/deltas-mattermost.json tools/.deltas-state.json +bash tools/run-project-deltas.sh apply tools/deltas-mattermost.json tools/.deltas-state.json +bash tools/run-project-deltas.sh list tools/deltas-mattermost.json tools/.deltas-state.json +``` + +📌 `check` **обязателен перед `apply`** — он показывает TODO/MISSING/OK по каждому якорю. +Именно на этой проверке я поймал, что дельта валидации указывала в несуществующий +`validate()` в `config.go` (валидация живёт в `app.go`) и переписал её до записи. + +⚠️ **Не писать `search` с литеральными секретами** — значения собираются на хосте. +⚠️ Дельты с длинным `replace` писать **в JSON-файл через `write_file`**, не в heredoc: +кавычки и `\n` ломаются. + +--- + +## Объём работ + +Оценка из плана: **2-4 дня**. Разбивка: + +| Шаг | Объём | +|---|---| +| `client.go` (REST v4: posts, threads, files, reactions, websocket) | 1 день | +| `locator_codec.go` + `inbound.go` | 0.5 дня | +| `adapter.go` (все 6 операций + MM-специфика) | 1 день | +| `server.go` (WS ingress, reconnect, семафор, recover) | 1 день | +| `mattermostfx/` + presentation + prompt + structured | 0.5 дня | +| тесты (`go test -race ./...`) + `golangci-lint` | 0.5-1 день | + +### Quality gates (обязательны, из `AGENTS.md`) + +```bash +go test -race ./... +go tool golangci-lint run +``` + +### Логирование (правило проекта) + +Разрешено: `zerolog`, `log/slog`. Запрещено: `logrus`, `zap`, stdlib `log`. +Инициализация через `internal/logging.Init()`. Предпочитать структурированные поля. +Компонент логировать как `logger.With().Str("component", "balda.channel.mattermost").Logger()`. + +### Коммиты + +Conventional Commits. Синхронизация общих веток — **merge (`git pull --no-rebase`), не rebase**. + +--- + +## Что понадобится из Mattermost API + +| Задача | Endpoint | +|---|---| +| Отправка поста | `POST /api/v4/posts` (`{channel_id, message, root_id?, file_ids?}`) | +| Редактирование (draft→update) | `PUT /api/v4/posts/{post_id}` | +| Треды | `root_id` в create — ответы в тред | +| Файлы | `POST /api/v4/files` + `file_ids` в посте | +| Реакции (для question-контролов) | `POST /api/v4/reactions` | +| WebSocket события | `wss:///api/v4/websocket` (`posted`, `post_edited`) | +| Ping / health | `GET /api/v4/system/ping` | + +Авторизация: **Bot account + Personal Access Token** (`Authorization: Bearer `). +⚠️ Alex сказал: **бота создавать не надо** — токен предоставит он сам. + +## Открытые вопросы / решения Alex + +- **Бота не создавать** (2026-09-26, явно). Токен для MM-адаптера даст Alex. +- **Вопросы не задавать** — решения принимать самому, искать в Obsidian, а не спрашивать. +- **Команд, требующих подтверждения, не использовать** — писать скрипты. +- **Не останавливаться** до исполнения задачи. + +## Связанные заметки + +- [[personal/projects/balda/mattermost-kraken-deploy]] — сервер MM на Kraken (Фаза 1, ✅ закрыта) +- [[personal/projects/balda/balda-mm-delegation-plan]] — общий план +- [[personal/tech/balda-go-fx-pitfalls]] — 🔴 fx-питфоллы (fx.In/ParamTags, провайдеры) + метод дельт +- [[personal/projects/balda/balda-valera]] — текущая эксплуатация Балды/Валеры +- [[personal/tech/hermes-secret-redaction-bypass]] — как обходить маскировку секретов diff --git a/personal/projects/archive/callee-prism-contracts-2026-09-29.md b/personal/projects/archive/callee-prism-contracts-2026-09-29.md new file mode 100644 index 00000000..d6ae83db --- /dev/null +++ b/personal/projects/archive/callee-prism-contracts-2026-09-29.md @@ -0,0 +1,393 @@ +# Callee + Prism — историческая справка по контрактам + +> [!WARNING] Версионная справка, 2026-09-29 +> Контракты ниже относятся к исследованным тогда версиям. Их нельзя считать описанием установленного Broker без сверки закреплённых зависимостей, workflow и worker package. +> Проверенные версии, текущие образы, реальные прогоны и оставшаяся работа: [[personal/projects/balda/execution-broker]]. +> Старый [[personal/plans/balda-callee-prism-plan|план интеграции]] сохранён как архив. + +> Создан 2026-09-29. Опорная заметка к [[personal/plans/balda-callee-prism-plan]]. +> Назначение: справочник по контрактам callee и prism, чтобы не перечитывать +> доки апстрима при каждой правке ресурсов. + +--- + +## 1. Что это + +| Инструмент | Репозиторий | Лицензия | Что делает | +|---|---|---|---| +| **callee** | `baldaworks/callee` | MIT | воркфлоу как версионируемые Markdown/YAML-ресурсы | +| **prism** | `baldaworks/prism` | MIT | плагин в coding agent: жизненный цикл Story/Epic | +| **prism-callee** | `baldaworks/prism-callee` | MIT | те же воркфлоу через callee-агентов | +| **Beads** | `gastownhall/beads` | — | durable-состояние (`bd`), носитель состояния prism | + +📌 Версия callee на 2026-09-29 — **`0.22.2`** (npm `@baldaworks/callee`, meta-пакет +с optionalDependencies на платформенные бинарники: `darwin-arm64`, `darwin-x64`, +`linux-arm64`, `linux-x64`, `win32-x64`). + +--- + +## 2. callee — контракты + +### 2.1 Типы узлов (10 публичных kinds, `callee.metalagman.dev/v1alpha1`) + +| Группа | Kind | Поведение | +|---|---|---| +| Листья | `Role` | промпт + один или несколько ходов в одной свежей ACP-сессии | +| | `DynamicRole` | то же, но конфиг провайдера рендерится из состояния визита | +| | `Script` | детерминированный локальный шаг через `sh`/`bash` | +| | `Human` | пауза, один непустой ответ оператора | +| Типизированные оценки | `TypeSafeJev` | нативный оценщик System One/Jev | +| | `OpenRouterDecision` | OpenRouter Decisions API | +| Композиты | `Sequential` | дети один раз, в порядке исходника | +| | `Parallel` | fan-out и join | +| | `Loop` | повтор детей в границах до эскалации | +| | `Router` | ровно один именованный или дефолтный ребёнок | + +### 2.2 Исполнение + +``` +Markdown/YAML + → decode + schema/semantic/template validation + → registry + static graph resolution + ⛔ отвергает: невалидные ресурсы, битые ссылки, циклы, + дубли resource ID, дубли effective ID в resolved tree + → resolved root tree + required parameter keys + → runner + ОДИН shared ephemeral state-объект + → ОДИН корневой артефакт в stdout (пишется ПОСЛЕ cleanup) +``` + +⚠️ Диагностика идёт в **stderr**, артефакт — в **stdout**. Непустой stderr **не** означает +провал. Успех определять по **exit status**. + +**Порядок визита узла:** счётчик → слияние `spec.state` со state входящего ребра → +рендер строковых листьев по **одному** pre-node снапшоту → атомарный коммит state- +модификатора → поведение по типу → `agent finished` со статусом и длительностью. + +🔴 При провале рендера state: **ни одна часть модификатора не коммитится, провайдер не стартует.** + +### 2.3 Провайдеры — ЗАКРЫТЫЙ список + +| `type` | Команда по умолчанию | Предусловие | +|---|---|---| +| `codex` | сам callee + `bridge codex` | авторизованный Codex CLI | +| `claude` | `npx -y @zed-industries/claude-code-acp@latest` | Node + npx + креды адаптера | +| `opencode` | `opencode acp` | OpenCode CLI | +| `copilot` | `copilot --acp --stdio` | Copilot CLI | +| `grok` | `grok agent stdio` | Grok CLI или `XAI_API_KEY` | +| `cursor` | `agent acp` | Cursor CLI | +| `generic_acp` | **нет — требует непустой `cmd`** | любой ACP-исполняемый файл | + +🔴 **Gemini не является валидным типом.** DeepSeek — тоже нет → только через `generic_acp`. + +**Поля провайдера:** + +| Поле | Контракт | +|---|---| +| `type` | обязателен, из закрытого списка | +| `cmd` | **одно имя исполняемого файла, НЕ shell-строка**; для `generic_acp` обязателен | +| `model` / `reasoning` / `mode` | опциональны; пустые → дефолт бэкенда | +| `extraArgs` | упорядоченный argv-список; каждый элемент непустой | +| `timeout` | Go duration, **по умолчанию `15m`** | + +⚠️ Конфиг провайдера обязан быть **вложенным** — плоские поля в `spec` не поддержаны. + +**Таймаут независим для:** старта процесса, создания и подготовки сессии, **каждого** +хода. Корневой прогон целиком он **не** ограничивает. + +**Переиспользование процесса:** одинаковые effective `type` + полностью резолвленный `cmd` +→ один процесс. `model`/`mode`/`reasoning` на идентичность процесса **не влияют**. +Сессия всегда создаётся свежая. + +### 2.4 DynamicRole — провайдер из состояния + +Все поля провайдера — шаблоны: `type`, `cmd`, `model`, `reasoning`, `mode`, `timeout`, +**каждый** элемент `extraArgs`. + +| Факт | Значение | +|---|---| +| Источники | `.Prompt`, текущий `.Input`, **один** согласованный post-node снапшот `.State`, `.Params` | +| Недоступно | `.Output`, env, файловая система, сеть, часы, рандом, криптография, мутаторы | +| Рендер | ровно **один раз на визит**; валидируется **до** поиска процесса | +| Периметр | конфиг фиксирован на весь визит, включая REPL-ходы | +| Повтор | новый визит Loop рендерит заново из актуального состояния | +| Ошибка | рендер или валидация падают → визит падает, ничего не стартует | + +📌 В `callee agent view` DynamicRole помечается `provider=runtime`; JSON-view сохраняет +авторские шаблоны. Обычный `callee doctor` валидирует ресурс, шаблон и граф, но готовность +провайдера **откладывает** — исполняемый файл проверяется только при достижении узла. + +📌 После рендера в lifecycle-событиях появляются `role_model` и `role_reasoning`. +**До** успешного рендера эти поля опускаются — их появление = рендер прошёл. + +🔴 **Безопасность:** state-driven `cmd` и `extraArgs` могут выбирать исполняемый код. +Не подавать туда недоверенный вывод модели без ограничения авторской шаблонной логикой. +`cmd` остаётся одним исполняемым файлом, `extraArgs` — argv-списком; shell не участвует. + +### 2.5 Script и Human + +**Script:** результат пишется в `State.scripts[effectiveId]` — `status` (`passed`/`failed`), +`exitCode` (`-1` при таймауте), `stdout`, `stderr`, `timedOut`. +🔴 **Таймаут всегда валит воркфлоу.** Ненулевой exit валит, если не задан `onNonZero: continue`. + +**Human:** рендерит тело, показывает на **управляющем терминале**, просит один непустой +ответ. Пишется в `State[responseKey]` и дублируется в `State.outputs[effectiveId]`. + +> 🔴 **Requires interactive mode** and an interactor backed by a controlling terminal. +> Explicit non-interactive mode **rejects a Human anywhere in the resolved tree during +> preflight, including beneath an unselected Router branch**. + +**Следствие:** воркфлоу с `Human` **не запускается** в non-interactive режиме вообще — +и проверка идёт до всякой логики, по всему resolved tree. Запуск через `docker exec` +без `-t` даёт non-interactive → `Human` невозможен. + +### 2.6 Эскалация + +Принадлежит **рёбрам**, а не определениям ресурсов. `Role` может эскалировать в ближайший +включающий `Loop` **только если каждое** ребро от Loop к этому вхождению имеет +`canEscalate: true` (опущенное = `false`). Вложенный `Loop` открывает **новую границу +авторизации** — потомки не наследуют полномочия внешнего. + +### 2.7 Команды + +```bash +npx --yes @baldaworks/callee@latest setup +callee agent list +callee agent view [--json] +callee agent run --message "..." [--repl-timeout D] +callee agent import --path --prefix [--force] +callee doctor +``` + +--- + +## 3. Prism — таск-трекинг + +### 3.1 Жизненные циклы + +**Story:** Specify → Design → Breakdown → **Human approval** → Apply → Verify. +Фазы: `phase:story:specify` → `design` → `breakdown` → `human` → `apply` → `verify`. + +**Epic:** Frame → Architecture → Roadmap → **approval** → Deliver Stories → Validation. +Фазы: `phase:epic:frame` → `architecture` → `roadmap` → `approval` → `delivery` → `validation`. + +### 3.2 Правила + +| Правило | Значение | +|---|---| +| Дети Epic | только **Stories** | +| Дети Story | только **Tasks** | +| Вложенные Epic, Task напрямую в Epic | **невалидно** | +| Фиксированный диапазон числа детей | **нет** — оценка качественная (покрытие, связность, ревьюимость, верифицируемость, ацикличность) | +| `Apply` | 🔴 **каждый переход требует явной авторизации человека** | +| Одобрение Epic → Story | **не переносится** | +| Критерии приёмки | показываются **только** по явному запросу оператора | + +При паузах prism останавливается только на: необходимом уточнении, одобрении перед +реализацией, блокерах. + +### 3.3 Установка и импорт + +**В coding agent** (независимая установка, например Codex): + +```bash +codex plugin marketplace add baldaworks/prism +codex plugin add prism@prism +# обновление: codex plugin marketplace upgrade prism → повторить add → новый thread +``` + +**Callee-агенты** (отдельный репозиторий, требует `callee >= 0.19.0` + `bd`): + +```bash +callee agent import baldaworks/prism-callee \ + --path pack/callee/prism \ + --prefix prism +# обновление/починка: тот же вызов + --force +``` + +**Проверка:** + +```bash +callee agent list | grep '^prism/' +callee agent view prism/lifecycle --json # 🔴 kind ДОЛЖЕН быть Router +callee agent view prism/story --json +callee agent view prism/epic --json +bd where +``` + +🔴 **`Sequential` вместо `Router` = устаревший импорт** → обновлять `--force`. +`--agent-root pack/callee` — только для поддержки самого репозитория. + +> «Each repository installs and validates independently — installing one plugin does +> not install the other.» + +### 3.4 Штатный и прямой запуск + +| Способ | Команда | Что даёт | +|---|---|---| +| Штатный (рекомендован) | `$prism-callee:lifecycle <запрос>` | резолвит Beads-цель, **персистит фазовые метки**, поддерживает resume | +| Прямой Story | `callee agent run prism/story --message "..."` | исполняет граф, но **не** резолвит Beads-элемент и не персистит метки | +| Прямой Epic | `callee agent run prism/epic --message "..."` | то же ограничение | + +📌 «The coding agent owns durable state and approval. Callee executes the selected graph; +**approval never transfers from an Epic to a Story.**» + +--- + +## 3.5 Роли: что это и как ими управляют + +**Роль — YAML/MD-файл в каталоге**, не право и не токен. Описание листа графа: провайдер, +промпт, параметры, таймаут. + +```yaml +apiVersion: callee.metalagman.dev/v1alpha1 +kind: Role +spec: + description: Собирает Xcode-проект + provider: + type: generic_acp + cmd: dsh-acp + model: deepseek-chat + timeout: 20m +--- +Собери проект {{ .Params.project }} и верни отчёт об ошибках. +``` + +| Источник ролей | Что даёт | +|---|---| +| Каталог по умолчанию | 6 стартовых ресурсов (`callee setup`) | +| Импорт из git | `callee agent import baldaworks/prism-callee --prefix prism` | +| Собственные | файлы в каталоге проекта или `~/.callee/` | + +🔴 **Ролей в рантайме не выдают.** Роль = наличие файла в каталоге. Рантайм-механизма +«выдать роль на задачу» **не существует**. + +### Что динамическое, а что нет + +| Сущность | Динамически? | Чем регулируется | +|---|---|---| +| Роль (файл графа) | ❌ статично | наличие файла в каталоге | +| Провайдер (`type`, `cmd`) | ❌ или из `State` через `DynamicRole` | автор графа | +| Файлы | ❌ | монтирование + рабочая копия | +| SSH-ключ | ❌ | наличие файла у процесса | +| **Разрешение модели внутри ACP-сессии** | ⚠️ **да** | `allow_all` / `ask` / `deny_all` | + +🔴 **Единственное, что реально запрашивается в рантайме — разрешение модели внутри +ACP-сессии** (`ask` → вопрос в чат). + +--- + +## 4. Права: формула и границы + +**`права = права процесса × смонтированное × разрешённое в графе`** + +| Уровень | Кто задаёт | +|---|---| +| Чем запущен процесс | вы, при старте (контейнер / хост-юзер / отдельный юзер) | +| Что смонтировано | compose | +| Что разрешено в графе | автор воркфлоу | + +🔴 **`permissions.mode: allow_all` — это про права модели ВНУТРИ ACP-сессии** (править +файлы, запускать команды). Оно **не** расширяет, что процесс видит в файловой системе. +Два разных ограничения. + +### Где callee — там и движки, и Script'ы + +🔴 **ACP-провайдер — дочерний процесс callee.** Движок на хосте + callee в контейнере +технически невозможны. Следствие: «callee на хосте» = **всё исполнение на хосте целиком**. + +### Гейт `Human` защищает действие, но не возможность + +Доступ у роли есть **всегда** — он определён тем, чем запущен callee и что смонтировано. +Апрув **не выдаёт доступ**, он разрешает выполнить операцию, которая уже технически +возможна. + +``` +Роль лежит в каталоге → доступ ЕСТЬ + ▼ +Узел Human («собрать Xcode в VM macos-27?») → пауза + ├─ апрув → Script исполняется (права у процесса УЖЕ были) + └─ отказ → граф останавливается +``` + +🔴 **Изоляцию даёт только то, чем процесс запущен и что смонтировано — не граф.** + +### Динамических маунтов не существует + +На macOS Docker маунты задаются **при создании контейнера**; live `mount --bind` +контейнер не видит; симлинк на внешний путь битый. **«Как модель запросит доступ к папке» +не решается через маунты.** + +| Подход | Оценка | +|---|---| +| A. Смонтировать всё заранее (`~/Developer:ro`) | запись нельзя | +| B. Пересоздание контейнера (`up -d`, ~3 с) | нужен механизм вне контейнера | +| C. Хост-агент ставит права сам | полный доступ, ноль изоляции | +| **D. Проект = рабочая копия** ✅ | модель работает в `~/agents/<проект>`, результат в проект через git | + +**Решение: D** — доступ задаётся наличием репозитория, а не маунтом. + +### Доступ выбирается маршрутом, а не запросом + +Модель **не просит** прав и **не может эскалировать**: нет доступа — задача падает. + +``` +Роутер в графе + ├─ задача требует ~/agents → роль в контейнере + ├─ задача требует Xcode/lume → роль на хост-исполнителе + └─ задача требует хоста по ssh → роль со ssh-ключом (command=) +``` + +**Разграничение = статический роутинг по типам задач**, не динамические права. + +--- + +## 5. DeepSeek как ACP-провайдер + +Провайдер callee — `generic_acp`, `cmd: dsh-acp`, пакет +**`@openma/deepseek-harness-acp@0.4.33`** (Apache-2.0, Node `>= 22.15`, +`bin: dsh-acp` → `dist/bin.js`). Обёртка вокруг официального DeepSeek Harness. + +| Способ передачи кред | Значение | +|---|---| +| Флаг | `api-key`, либо `api-key:` при нескольких провайдерах | +| Env | **`DEEPSEEK_API_KEY`** | +| Env | **`DEEPSEEK_BASE_URL`** — OpenAI-совместимый endpoint override | + +Приоритет: **флаги → переменные окружения → дефолты**. + +📌 `DEEPSEEK_BASE_URL` — OpenAI-совместимый override → смена провайдера сводится +к смене одной переменной, без правки графа. + +📌 Официальный `@deepseek-ai/dsh-acp` **не подошёл**: pre-release (последние версии +сплошь `-rc`/`-alpha`), `bin: null` — библиотека, а не запускаемый сервер. + +--- + +## 6. Питфоллы + +1. 🔴 **`Human` требует TTY** — non-interactive режим отвергает граф целиком, включая + невыбранные ветки Router. Запуск через `docker exec` без `-t` = non-interactive. +2. 🔴 **`cmd` — одно имя файла.** Аргументы только в `extraArgs`, иначе провайдер не стартует. +3. 🔴 **Не подавать вывод модели в `cmd`/`extraArgs`** — исполнение произвольного кода. +4. 🔴 **ACP-провайдер — дочерний процесс callee** — движок и callee не могут быть на + разных хостах. +5. 🔴 **Гейт `Human` не выдаёт доступ** — только разрешает уже возможное действие. +6. ⚠️ **`prism/lifecycle` обязан быть `Router`** — иначе устаревший импорт. +7. ⚠️ **Непустой stderr ≠ провал** — судить по exit status. +8. ⚠️ **Таймаут Script валит весь воркфлоу** — не «продолжает с ошибкой». +9. ⚠️ **Concurrency группы `Parallel`** используют один живой shared state. +10. ⚠️ **Поиск npm по `scope:normahq` / `scope:baldaworks` даёт пусто** — эти scoped-пакеты + через search-API не индексируются. Искать прямыми URL + `registry.npmjs.org/%2f`. +11. ⚠️ **Динамических маунтов на macOS Docker нет** — только пересоздание контейнера. + +--- + +## Связанные заметки + +- [[personal/plans/balda-callee-prism-plan]] — план внедрения (топология: стек на Mac) +- [[personal/projects/balda/balda-mm-delegation-plan]] — общий план Фаз 1-5 +- [[personal/plans/balda-mm-phase4-agents]] — Фаза 4 (мост, приёмка) +- [[personal/tech/agents-bridge]] — мост :8790 (демонтируется в новой топологии) +- [[personal/tech/balda-executor-mcp]] — MCP-сервер делегирования (демонтируется) +- [[personal/tech/macos-docker-virtiofs-mount-limits]] — почему нет динамических маунтов diff --git a/personal/projects/balda/balda-mattermost-connector.md b/personal/projects/balda/balda-mattermost-connector.md old mode 100755 new mode 100644 index d99e33bb..8c888ae2 --- a/personal/projects/balda/balda-mattermost-connector.md +++ b/personal/projects/balda/balda-mattermost-connector.md @@ -1,810 +1,13 @@ -# Balda — Mattermost-коннектор: исторический дизайн +# Balda — Mattermost в текущем контуре -> [!WARNING] Историческая спецификация -> Код Mattermost-коннектора принят в [PR #8](https://github.com/baldaworks/balda/pull/8), merged **2026-09-29**; статус перепроверен **2026-10-14**. -> Схема, пути, команды и результаты ниже описывают версию сентября и не служат текущим runbook. -> Текущий запуск: [[personal/projects/balda/setup]]. Текущие PR и код: [[personal/projects/balda/balda]]. Broker/ingress recovery и приёмка: [[personal/projects/balda/execution-broker]]. +Проверено: **2026-10-15**. Два инстанса на Mac: `balda-mm-mac` и `balda-mm-eagle`. Конфиги, образы, сети, MCP и команды эксплуатации: [[personal/projects/balda/setup]]. Приёмка Broker: [[personal/projects/balda/execution-broker]]. -> Создано 2026-09-26. Обновлено 2026-09-28 (правки по ревью применены). -> Статус PR: **MERGED, 2026-09-29**. Приведённые ниже CI и commit IDs — история приёмки PR #8. -> 🟢 **PR:** https://github.com/baldaworks/balda/pull/8 — 46 файлов, +6850/−27, 9 коммитов. -> Фаза 2 закрыта полностью: код, 103+ новых теста, доки, зелёный CI. -> Блокер `tools/` снят: `/tools/` в `.gitignore` + `git rm -r --cached tools/`. -> Детали — [[personal/plans/balda-mm-pr-plan]]. -> 🔧 **2026-09-28 — правки по ревью `metalagman`:** голова `05342c5a`, CI 8/8 PASS. -> Закрыты slash-команды (новый HTTP-приёмник `command_server.go`), `/locator` -> (`locatorref` + guard-тест), групповой DM (сессия по чату), и **`ScopeResolver`-регистрация, -> которой не было в PR**. Разбор — [[personal/plans/balda-mm-pr8-review-fixes]]. -> Часть проекта [[personal/projects/balda/balda-mm-delegation-plan]] (Фаза 2). -> Требование Alex: поддержка **всех** функций Балды, по новым API, production-качество. -> -> **Проверено (2026-09-26, PR):** -> - `go build ./...` → exit 0 · `go vet ./...` → exit 0 -> - `go tool golangci-lint run` → **0 issues** -> - `go tool go-arch-lint check --project-path .` → **OK - No warnings found** -> - **DI-граф собирается целиком** с `balda.mattermost.enabled=true` — проверено -> `validateBaldaApplication(prepared)` через fx, а не только компиляцией -> - **103 новых теста** зелёные, включая `-race`; в CI — 126 PASS, 0 FAIL, 0 SKIP -> - **CI: 8/8 джобов PASS** на чистом раннере (`.github/workflows`) -> -> 🔴 **Исторический урок (Фаза 3):** «все quality gates пройдены» оказалось неполным — -> при первом **живом запуске** на Kraken Балда ушла в crash-loop: -> `advertised commands have no actor handler: goal`. В `Advertisement.Names` была объявлена -> несуществующая команда `goal` (это payload kind, а не имя хендлера; цикл называется -> `goalkeeper`). **Ни `go build`, ни `go vet`, ни `golangci-lint`, ни `go-arch-lint`, ни -> `validateBaldaApplication` этого не увидели.** Исправлено в `mattermostfx/module.go` и -> `inbound.go` дельтой `tools/deltas-goal-command-fix.json`. Разбор — -> [[personal/tech/balda-go-fx-pitfalls]] §«Дефект 4». -> 📌 Вывод: «Фаза 2 закрыта» означала «код компилируется и граф собирается», но **не** -> «транспорт работает». Живой запуск обязателен. Статус Фазы 3 — -> [[personal/projects/balda/balda-kraken-mm-deploy]]. -> -## ✅ PR-ПРОХОД 2026-09-26 — транспорт приведён к PR-качеству +Канонический канал Mattermost-сессии — `mattermost`. Доставка owner approval через Telegram не меняет locator исходной беседы. Возврат Broker идёт через `balda.state` и native `balda.session.wait`, зарегистрированные до Start/retry. Старые callback routes Broker приостановлены с сохранением cursors. -Ветка `feat/mattermost-channel`, **7 коммитов**, PR **открыт** — baldaworks/balda#8. -Подробности и полный список — [[personal/plans/balda-mm-pr-plan]]. +Реальная беседа проверена с фактическим owner approval и отдельно с outage/restart: сохранились исходные session/provider thread, run, Story и deadline; подтверждены артефакты с SHA256, `cleanup=done`, закрытие Story/Task и единственный итоговый ответ в исходной Mattermost-беседе. Сервисный fixture и native wait сами по себе не доказывают доставку сообщения провайдером. -``` -9f1e6c9c chore: ignore local project-editing tooling ← уборка tools/ из PR -58f27120 docs(mattermost): document the transport setup and behavior -80d4f41d test(mattermost): cover the transport, the ingress and the handler -c7f18654 fix(mattermost): send a monotonic seq on every client frame -6dc3077a feat(mattermost): add the inbound handler and wire it into DI -fa1a2855 feat(auth): support the Mattermost channel subject -004fda2f feat(mattermost): add Mattermost transport adapter ← база прошлой сессии -``` +В текущем Broker Telegram approval timeout — 1800 секунд, minimum run TTL — 3600 секунд. Кнопки добавляются после durable регистрации сообщения и удаляются после settlement; восстановление публикации не зависит от повторного inbound. Тестовые API-approval могут автоматически закрываться до чтения сообщения владельцем. -**Исправлено в коде (не в данных):** +Токены не записываются в заметки. При обновлении сохраняются home volumes, provider credentials и история. Модели и аккаунты не копируются между инстансами. Для общих инструментов действует одинаковая policy; Asana у Eagle имеет отдельный readonly MCP. -| Дефект | Фикс | -|---|---| -| 🔴 Двойной префикс `principal` — прошлый фикс был в БД и вернулся | `auth.MattermostSubject()` теперь **идемпотентен** (срезает `mattermost:` перед повторной квалификацией); из `handler.go` убран `TrimPrefix`-костыль | -| `ProgressPolicy.Typing: true` в command-пути | Приведено к `false` — у ботов MM нет typing API, `SendTyping` осознанный no-op | - -**Тесты — 6 новых файлов, 103 теста:** `locator_codec_test.go` (15), `inbound_test.go` (18), -`adapter_test.go` (17), `client_test.go` (13), `ingress_test.go` (23) + -`handlersfx/mattermost_ingress_test.go` (17). Ключевые регрессионные guard'ы: -`writeEvent` всегда с `seq` и уникальный `seq` под 16 горутинами (`bad_seq`), -plain-fallback только на content-rejection (не на 5xx), идемпотентность subject'а, -тред = одна сессия ≠ канальной, `/start` доступен до авторизации. -Пакеты `mattermost` + `handlersfx` в verbose-прогоне: **126 PASS, 0 FAIL, 0 SKIP**. - -**Доки:** README + `docs/balda.md` + новый **`docs/mattermost.md`** (создание бота, -минтинг токена, membership, 5 ключей, поведение, troubleshooting). - -**Все гейты зелёные на закоммиченном дереве:** -``` -go build ./... → 0 -go vet ./... → 0 -go tool golangci-lint run → 0 issues -go tool go-arch-lint check --project-path . → OK - No warnings found -go test -race (затронутые пакеты) → все ok -``` -⚠️ 5 пакетов падают локально (`cmd/balda`, `apps/balda`, `mcpruntime`, `session`, `state`) — -**это апстримная база**, проверено `git stash` (те же 4 теста падают без правок). -🔴 **В CI эти же тесты ПРОШЛИ** — локальное падение было особенностью машины, не дефектом. - -**`tools/` вычищен из ветки:** `/tools/` в `.gitignore` + `git rm -r --cached tools/` -(коммит `9f1e6c9c`). Файлы на диске целы (39 шт.), в PR-diff их нет. -⚠️ `gitignore` **не отвязывает** уже отслеживаемые файлы — нужны оба шага. - ---- - -## Статус Фазы 2: код закрыт (исторический раздел ниже) - -> Разделы ниже — сначала контракт/разведка, затем **фактически написанные файлы**. Ищи -> §«Что фактически написано» для реестра файлов. -> -> 🔧 **Инструмент правок:** `tools/apply-project-deltas.py` + `tools/run-project-deltas.sh` + -> `tools/deltas-mattermost*.json` — правки проекта идут **текстовыми дельтами с якорями**, -> не sed/in-place. См. §«Метод правок: дельта-апплаер». - ---- - -## Где работаем - -| | | -|---|---| -| Репозиторий | `~/Developer/balda` (форк `mallexxx/balda`) | -| Ветка | **`feat/mattermost-channel`** — создана от **чистого `upstream/main`** | -| База | `de273d8f feat(balda): embed backoffice in startup lifecycle` | -| Апстрим | `baldaworks/balda` (`normahq/balda` редиректит туда — SHA идентичны) | -| Наши коммиты | **не тащим** (отставание 426 коммитов; решение в плане) | - -Проверка: `git rev-list --left-right --count main...upstream/main` → `2 426`. - -⚠️ В форке остаётся `5ad65955` — `replace normahq/norma => ./norma-local` в `go.mod`. -На ветке от upstream его нет (ветка чистая), но при любом мерже с нашей `main` — удалять. - ---- - -## Контракт: `docs/architecture/transport-presentation-boundary.md` - -Ключевое правило (generic invariant для транспорта `X`): -- `X` может зависеть от shared-контрактов; shared-контракты **не могут** зависеть от `X`; -- внешний wiring импортирует `X` (root) и `Xfx`, **но не** внутренние `presentation`/`delivery`; -- всё UX-специфичное живёт **внутри** `X`. - -Два пути вывода: -1. **Model-authored text** → prompt-format путь. Транспорт может инжектить свои правила форматирования. -2. **System-authored service messages** → типизированные дескрипторы + детерминированные рендереры, - регистрируемые каждым транспортом отдельно. **Не** в shared-пакетах. - ---- - -## Образец: `internal/apps/balda/channel/zulip/` - -Размеры (эталон объёма для MM): - -| Файл | Строк | -|---|---| -| `adapter.go` | 331 | -| `adapter_test.go` | ~235 | -| `client.go` | 289 | -| `server.go` | 316 | -| `server_test.go` | 593 | -| `locator_codec.go` | 229 | -| `inbound.go` | 192 | -| `formatting.go` | 14 | -| `presentation/locator.go` | 18 | -| `presentation/goal.go` | 76 | -| `zulipfx/{module,prompt,structured}.go` | 66 + 36 + 54 | - -### `zulipfx/module.go` — что регистрируется через DI - -```go -var Module = fx.Module("balda_channel_zulip_fx", - fx.Provide( - // 1. Advertisement — список поддерживаемых команд для транспорта - fx.Annotate(func(enabled bool) commandcmd.Advertisement { - return commandcmd.Advertisement{Transport: zulip.ChannelType, Enabled: enabled, - Names: []string{"locator","reset","usage","auto","cancel","goalkeeper","topic","close","start","user","skill"}} - }, fx.ParamTags(`name:"balda_zulip_webhook_enabled"`), - fx.ResultTags(`group:"balda_command_advertisements"`)), - - zulip.NewServer, // 2. ingress carrier - func(client *zulip.Client, logger zerolog.Logger) *zulip.Adapter { - adapter := zulip.NewAdapter(client, logger) - adapter.SetTypingThrottleInterval(4 * time.Second) - return adapter - }, // 3. delivery adapter - - fx.Annotate(func(a *zulip.Adapter) deliveryfx.ChannelAdapterBinding { - return deliveryfx.ChannelAdapterBinding{ChannelType: zulip.ChannelType, Adapter: a} - }, fx.ResultTags(`group:"balda_delivery_channel_adapter"`)), // 4. binding в роутер - - fx.Annotate(NewPromptRegistryContribution, - fx.ResultTags(`group:"balda_delivery_prompt_contribution"`)), // 5. prompt-правила - - fx.Annotate(NewLocatorStructuredRegistrar, // 6. structured renderers - fx.ResultTags(`group:"balda_delivery_structured_registrar"`)), - fx.Annotate(NewGoalProgressStructuredRegistrar, …), - fx.Annotate(NewGoalOutcomeStructuredRegistrar, …), - - fx.Annotate(func(h *zulip.Server) appports.TransportLifecycleStage { - return appports.TransportLifecycleStage{Name: "zulip ingress", Start: h.Start, Stop: h.Stop} - }, fx.ResultTags(`group:"balda_transport_lifecycle_stage"`)), // 7. lifecycle - ), -) -``` - -**7 групп DI — столько же нужно для MM.** Имена групп фиксированы. - -### `locator_codec.go` — модель локатора - -- `ChannelType = "zulip"` (MM: `"mattermost"`), `sessionIDPrefix = "zu"` (MM: `"mm"`). -- Address payload — JSON: `{type, stream_id, topic, user_id}`. MM: `{type, channel_id, root_id, user_id}`. -- AddressKey: `s::` / `dm:`. MM: `c:` / `dm:` / `t:`. -- SessionID: `zu-s--<8 hex от sha256(topic)>` / `zu-dm-`. -- Три обязательные функции: `NewXLMLocator`, `DecodeLocator` (возвращает `(addr, ok, err)`), - `LocatorFromAddressKey`, `ClassifyLocatorScope` → `LocatorScopePersonal` / `LocatorScopeGroup`. -- Дополнительно: `UserID(int) string` ↔ `ParseUserID(string) (int, error)` — префикс `zu-`/`mm-`. - -### `adapter.go` — `deliverycmd.Adapter` - -Реализует `var _ deliverycmd.Adapter = (*Adapter)(nil)`. Константы операций -(`deliverycmd.Operation*`), которые надо покрыть: `Plain`, `Markdown`, `AgentReply`, -`Draft`, `Typing`, `Progress`. Результат — `deliverycmd.Result{ProviderMessageID string}`. - -Обязательные публичные методы (по образцу Zulip — вызываются роутером и gateway): -`SendPlain`, `SendMarkdown`, `SendMarkdownWithFormat`, `SendAgentReply`, -`SendAgentReplyWithProviderMessageID`, `SendAgentReplyWithProviderMessageIDAndFormat`, -`SendDraftPlain`, `SendTyping`, `SendProgress`, `Deliver`. - -**Питфоллы, заложенные в Zulip-адаптере (повторить):** -- `sendWithPlainFallback` — если провайдер отбил markdown (`isContentRejectedError`), - отправить plain-вариант. Ошибки **объединять** через `errors.Join`, не терять первую. -- Fallback **только** на content-rejection (400/BAD_REQUEST), **не** на 5xx — иначе - дубли сообщений при временных сбоях. -- Typing **throttle** по `locator.SessionID` (Zulip — 4 с), с инъектируемым `now func() time.Time` - (нужно для тестов). Поле `typingLastSentAt map[string]time.Time` под mutex. -- `validateReady()` — явная проверка `client != nil`, возвращает человекочитаемую ошибку. -- `SendDraftPlain` для Zulip — **no-op** (`return nil`), т.к. нет edit-in-place. - Для **MM это не no-op**: у MM есть `PUT /posts/{id}` (edit) — реализовать по-настоящему. -- `SendProgress`: `ProgressThinking` → no-op; `ProgressPlanUpdate` → отправить текст. - -### `inbound.go` — нормализация входа - -Возвращает `turncmd.NormalizedInbound` с полями: `ID` (`turncmd.InboundID("zulip:"+msgID)`), -`Text`, `Locator`, `ProviderMessageID`, `UserID`, `MessageID`, `ReceivedAt` (RFC3339 UTC), -`DeliveryFormat`, `ProgressPolicy{Typing, PlanUpdates}`, `Direct`, `Source`. - -MM: `Source` = новый `turncmd.SourceMattermost`, `DeliveryFormat` = `DeliveryFormatMarkdown`, -`ProgressPolicy{Typing: false, PlanUpdates: true}` — **в MM нет typing-индикатора в каналах** -(есть только в DM через WebSocket `user_typing`; для каналов не работает). - -Также в `inbound.go`: `InboundMessage`, `InboundCommand`, интерфейс `InboundProcessor` -(3 метода: `ProcessInbound`, `HandleCommand`, `HandleUnsupportedCommand`), -`SupportedCommands() []string`, `commandSupported(name)`. - -### `ingress.go` — ingress (фактически реализовано) - -⚠️ **Zulip использует outgoing webhook по HTTP.** У MM для ботов — **WebSocket** -(`wss:///api/v4/websocket`). Это отличается от Zulip-образца принципиально. - -**Как реализована аутентификация (проверено компиляцией):** -- dial с заголовком `Authorization: Bearer `; -- **плюс** обязательный первый кадр `{"seq":1,"action":"authentication_challenge","data":{"token":""}}` — - без него MM не начинает слать события; -- `WebSocketURL()` в `client.go` сам конвертирует `http→ws` / `https→wss` и добавляет `/api/v4/websocket`. - -⚠️ В ранних заметках было `Sec-WebSocket-Protocol: bearer ` — **это не то, что реализовано -и не то, что описано в актуальных доках MM.** Использовать `authentication_challenge`. - -Перенесено из Zulip-сервера и работает: -- **семафор на конкурентную обработку** (`processSem chan struct{}`, 16) + `processWG`; - при заполнении — дроп с warn-логом, не накопление; -- **`defer recover()`** вокруг обработки поста → не ронять процесс; -- **фильтр эхо-сообщений** (`IsBotEcho`) → нет бесконечного цикла; -- **`SetReadLimit(1 MiB)`** на WS-кадр; -- **reconnect-loop** с backoff 5 с — `readLoop` переустанавливает соединение, пока контекст жив; -- **отдельная reader-горутина**: блокирующий `ReadMessage` не прерывается контекстом, поэтому - `Stop()` закрывает conn, и горутина выходит по ошибке; -- graceful `Stop()` с ожиданием активной обработки (`waitForProcessing`); -- `Start`/`Stop` в `appports.TransportLifecycleStage`. - -**Специфика MM, которой не было в Zulip:** -- В **канале** Балда отвечает только на `@mention` (`MentionsBot`/`StripMention`); в DM — на всё. -- События `post_edited` / `post_deleted` **осознанно игнорируются**: Балда действует по исходной - инструкции и не переписывает свой ответ задним числом. -- Системные посты (`Type != ""`) и удалённые отсеиваются до семафора. -- `data.post` в событии `posted` — это **JSON-строка**, а не объект (нужен двойной `Unmarshal`). -- Каналы типа `D` / `G` — это DM/групповой чат, а не канал команды. - -### `presentation/` + `formatting.go` - -- `formatting.go`: `FormattingPromptRuleAndExample() (rule, example string)` — - правило для модели (MM-совместимый Markdown), и `MarkdownPlainText(text)`. -- `presentation/locator.go`: `RenderLocator(body locatorfmt.Response) string` — локатор + YAML-блок - `target: locator` / `key: ` для scheduler/webhook. -- `presentation/goal.go`: `RenderGoalProgress(goalkeepercmd.GoalProgress)` и - `RenderGoalOutcome(goalkeepercmd.GoalOutcome)` — используют `ShouldRenderValidation()`, - `ShouldRenderVerified()`, `ShouldRenderNotVerified()`, `ShouldRenderNextAction()`. - -### `zulipfx/{prompt,structured}.go` - -- `prompt.go` → `deliveryfx.PromptRegistryContribution` с тремя полями: - `Formats` (правило+пример), `Formatters` (реализация `deliveryfmt.Formatter`: - `Name() Name` + `Format(text) (Message, error)`), `Routes` - (`{Transport, DeliveryFormat, RegisteredName}`). -- `structured.go` → по одному `deliveryfx.StructuredRegistryRegistrar` на каждый message kind - через `deliveryfx.NewStructuredRegistrar(deliveryfmt.TransportX, , renderer)`. - Renderer реализует `RenderStructured(ctx, deliveryfmt.StructuredEnvelope[T]) (StructuredPresentation, error)`. - Три штуки: `locatorfmt.ResponseDescriptor`, `goalkeepercmd.ProgressDescriptor`, - `goalkeepercmd.OutcomeDescriptor`. - ---- - -## Что нужно добавить в shared-слой — ✅ ВСЁ ПРИМЕНЕНО - -| Файл | Добавить | Статус | -|---|---|---| -| `deliverycmd/channel.go` | `ChannelTypeMattermost ChannelType = "mattermost"` | ✅ | -| `deliveryfmt/routes.go` | `TransportMattermost = "mattermost"`, `NameMattermostMarkdown Name = "mattermost_markdown"` | ✅ | -| `turncmd` (source) | `SourceMattermost` | ✅ | -| `docs/commands.md` | колонка Mattermost в таблице доступности команд | ✅ | -| `docs/reference/configuration.md` | 5 ключей `balda.mattermost.*` | ✅ | -| `README.md` / `docs/balda.md` | ⚠️ **не обновлялись** — README всё ещё перечисляет 3 chat surface | ⏳ | - -⚠️ `channel/router.go` **править не нужно** — он map-based по `locator.ChannelType`. -Достаточно зарегистрировать binding в DI (группа `balda_delivery_channel_adapter`). - -📌 Остаётся один незакрытый пункт требований `AGENTS.md` (не блокирующий): README и -`docs/balda.md` не упоминают Mattermost. Закрыть при подготовке PR в апстрим либо в Фазе 3. - ---- - -## Что фактически написано (2026-09-26) - -Всё в `~/Developer/balda` на ветке **`feat/mattermost-channel`**. `go build ./...` = exit 0. - -### Новый пакет `internal/apps/balda/channel/mattermost/` - -| Файл | Строк | Содержимое | -|---|---|---| -| `client.go` | ~560 | REST v4: `CreatePost`, `UpdatePost`, `GetPost`, `GetChannel`, `GetUser`, `GetMe`, `GetTeamByName`, `GetChannelByName`, `GetDirectChannel`, `UploadFile`, `CreatePostWithFiles`, `AddChannelMember`, `Ping`; `WebSocketURL()`, `APIBaseURL()`, `ValidateConfig()`; `APIError` | -| `locator_codec.go` | ~250 | `ChannelType="mattermost"`, префикс `mm`. AddressKey: **`c:` / `c::` / `d:`**. `NewChannelLocator(teamID, channelID, rootID)`, `NewDMLocator(channelID, userID)`, `DecodeLocator`, `LocatorFromAddressKey`, `ClassifyLocatorScope`, `ChannelIDFromLocator`, `RootIDFromLocator`, `IsDirectLocator`, `UserID`/`ParseUserID` | -| `inbound.go` | ~250 | `InboundMessage`/`InboundCommand`/`InboundProcessor` (3 метода), `NormalizeInbound`, `LocatorForPost`, `IsDirectChannelType` (`D`/`G`), `IsBotEcho`, `IsSystemPost`, `IsDeletedPost`, `StripMention`, `MentionsBot`, `ParseCommand`, `SupportedCommands`, `WebSocketEvent`/`PostedData` | -| `adapter.go` | ~420 | Все **9** `deliverycmd.Operation`: Plain, Markdown, AgentReply, Draft, Typing, Progress, **ClearQuestionControls, Photo, Document**. `sendWithPlainFallback`, `MarkdownPlainText`, `sendMedia` | -| `media.go` | ~45 | `openMediaFile` (лимит 100 МБ), `mediaBaseName` | -| `ingress.go` | ~380 | **WebSocket ingress**: dial с `Authorization: Bearer`, `authentication_challenge`, reconnect-loop, ping 30 с, `processSem` (16), `processWG`, `defer recover()` | -| `formatting.go` | ~14 | `FormattingPromptRuleAndExample()` | -| `presentation/presentation.go` | ~110 | `RenderLocator`, `RenderGoalProgress`, `RenderGoalOutcome` | -| `mattermostfx/module.go` | ~100 | 7 групп DI (как `zulipfx`) | -| `mattermostfx/config.go` | ~25 | `NewClient` + валидация при `enabled` | -| `mattermostfx/ingress.go` | ~40 | `IngressParams` (`fx.In`), `NewIngress` | -| `mattermostfx/structured.go` | ~100 | prompt contribution + 3 structured registrar | - -### Изменения в shared-слое (все применены) - -| Файл | Что добавлено | -|---|---| -| `deliverycmd/channel.go` | `ChannelTypeMattermost ChannelType = "mattermost"` | -| `deliveryfmt/routes.go` | `TransportMattermost = "mattermost"`, `NameMattermostMarkdown Name = "mattermost_markdown"`, маршрут `{TransportMattermost, DeliveryFormatNone, NamePlainText}` в `BuiltinRoutes()` | -| `turncmd/turn.go` | `SourceMattermost = "mattermost"` | -| `internal/apps/balda/app.go` | `mattermostfx.Module` в список модулей (строка ~667) | - -### Wiring-дельты (2026-09-26, вечер — закрыт блокер сборки) - -Это то, что оставалось «незакрытым» в первой половине дня. Применено дельта-апплаером. - -| Файл | Что добавлено | Зачем | -|---|---|---| -| `internal/apps/balda/app.go` | импорты `channel/mattermost` **и** `channel/mattermost/mattermostfx` | корневой пакет нужен для `mattermost.ValidateConfig`; `mattermostfx` — для `Module` | -| `internal/apps/balda/app.go` | **5 fx-провайдеров** зарегистрированных строк: `balda_mattermost_enabled`, `_server_url`, `_token`, `_bot_user_id`, `_bot_username` | модуль запрашивает их через `name:`-теги — без провайдеров fx падает «missing types» | -| `internal/apps/balda/app.go` | `validateMattermostConfig(cfg MattermostConfig) error` + вызов рядом с `validateSlackConfig` | fail-fast при `enabled=true` без URL/токена/bot_user_id | -| `internal/apps/balda/app.go` | функция вставлена **перед** `validateIdentifierValue` | порядок как у соседей | -| `internal/apps/balda/config.go` | поле `Mattermost MattermostConfig` в `BaldaConfig` + тип `MattermostConfig{Enabled, ServerURL, Token, BotUserID, BotUsername}` | конфиг-поверхность | -| `cmd/balda/command.go` | `!cfg.Balda.Mattermost.Enabled` в `validateBaldaChannelConfiguration` + подсказка про `BALDA_MATTERMOST_ENABLED` в тексте ошибки | иначе MM-only деплой отбивается «at least one channel is required» | -| `cmd/balda/balda.yaml` | дефолтный блок `balda.mattermost` с 5 ключами и комментариями | шаблон генератора | -| `docs/reference/configuration.md` | 5 строк описания `balda.mattermost.*` (перед `balda.slack.bot_token`) | документация ключей | -| `docs/commands.md` | абзац про поведение MM (mention в каналах, `/slash`-команды, нет typing) + колонка `Mattermost` в таблице + строка `/plugin ...` | таблица доступности команд | - -⚠️ **`app.go` правился в 4 захода** (импорт → провайдер enabled → валидация → остальные 4 строки). -Каждый заход — отдельная дельта-группа; всё в `tools/deltas-mattermost*.json`. - -### 🔴 Три реальных дефекта, найденных тестами (не компиляцией) - -Компиляция проходила, но fx-граф был сломан. Поймано только запуском -`validateBaldaApplication` с включённым Mattermost. - -**1. `fx.In` нельзя аннотировать внешним `fx.ParamTags`** — дефект в `mattermostfx/module.go`. - -Симптом: -``` -Failed: invalid annotation function func(mattermostfx.IngressParams) (*mattermost.Ingress, error): - fx.In structs cannot be annotated with fx.ParamTags or fx.From -``` - -Причина: в `module.go` `NewIngress` был обёрнут в `fx.Annotate(NewIngress, fx.ParamTags(...))`. -Но `IngressParams` **уже** содержит `fx.In` — его теги обязаны жить **внутри** структуры. - -**Правильно (как в `zulipfx`/`zulip.ServerParams`):** -```go -// ingress.go — теги на полях структуры -type IngressParams struct { - fx.In - Processor mattermost.InboundProcessor `optional:"true"` - Client *mattermost.Client - Commands *commandcmd.Registry - Enabled bool `name:"balda_mattermost_enabled"` - BotUserID string `name:"balda_mattermost_bot_user_id"` - BotUsername string `name:"balda_mattermost_bot_username"` - Logger zerolog.Logger -} - -// module.go — голое предоставление, БЕЗ fx.Annotate/fx.ParamTags -fx.Provide(NewIngress), -``` -⚠️ `Commands` был `*commandcmd.Registry `optional:"true"`` — заменён на обязательный -`*commandcmd.Registry` (как у Zulip). `commandcmd.Registry` в графе есть всегда. - -**2. Отсутствовали провайдеры зарегистрированных строк.** - -Симптом: `missing types: string[name="balda_mattermost_server_url"] (did you mean to Provide it?)` -×3 (server_url, token, bot_user_id). - -Причина: модуль читает значения через `fx.ParamTags`/`name:`, а провайдеров этих имён в -`app.go` не было. У Zulip они есть (`balda_zulip_*`) — для MM забыли. - -**Правильно:** 5 `fx.Provide(fx.Annotate(func() ..., fx.ResultTags(`name:"balda_mattermost_*"`)))`. -Для несуществующих ключей YAML `strings.TrimSpace` даёт `""` — безопасно, как у Zulip. -**Каждый новый `name:`-тег в модуле = обязательный провайдер в `app.go`.** - -**3. Отсутствовал импорт корневого пакета `mattermost`.** -`app.go:965: undefined: mattermost` — `validateMattermostConfig` вызывает -`mattermost.ValidateConfig`, а импортирован был только `mattermostfx`. - -### Тесты (написаны 2026-09-26) - -| Тест | Файл | Что проверяет | -|---|---|---| -| `TestLoadConfigDocument_AppliesMattermostEnvOverrides` | `cmd/balda/command_test.go` | 5 env-override `BALDA_MATTERMOST_*` доезжают в `doc.Balda.Mattermost` | -| `TestValidateCommandRunsValidationWithMattermostTransport` | `cmd/balda/command_test.go` | **реальный** `loadBaldaCommandConfig(true)` + `validateBaldaApplication(prepared)` — весь fx-граф с включённым MM | -| — (встроено в существующие) | — | регрессионная проверка `deliveryfmt` | - -### 🔴 Тесты транспорта и хендлера (2026-09-26, сессия PR-подготовки) - -Шесть новых файлов, ~90 тестов, все зелёные включая `-race`. - -| Файл | Кол-во | Что покрывает | -|---|---|---| -| `channel/mattermost/locator_codec_test.go` | ~16 | локаторы channel/thread/DM; **изоляция thread↔channel сессий**; `LocatorFromAddressKey`; `ClassifyLocatorScope`; `UserID`/`ParseUserID`; отказ чужим транспортам | -| `channel/mattermost/inbound_test.go` | ~17 | `NormalizeInbound` → shared-контракт; `Typing:false`; `LocatorForPost` (канал/тред/DM); фильтры; `ParseCommand`; whitelist (`goalkeeper` есть, `goal` нет); `SupportedCommands` возвращает копию | -| `channel/mattermost/adapter_test.go` | ~16 | все `Deliver`-операции; plain-fallback **только** на 400; draft-as-post; typing = no-op **без сети**; thread `root_id`; 5xx **не** ретраится | -| `channel/mattermost/client_test.go` | ~13 | ws/wss derivation; `Authorization: Bearer`; `APIError`; `isContentRejectedError`; контекст; 2xx без `id` = ошибка | -| `channel/mattermost/ingress_test.go` | ~20 | 🔴 **регрессия `bad_seq`**; монотонность/уникальность `seq` под 16 горутинами; все фильтры; `recover()`; семафор; `Start` отказ без `bot_user_id` | -| `handlersfx/mattermost_ingress_test.go` | ~18 | публикация всех команд; 🔴 **одиночный префикс principal**; `/start` до авторизации; отказ чужим без публикации; nil-safety | - -**Метод (для будущих транспортов):** - -- **REST** — `httptest.NewServer` + fake-handler на `POST /api/v4/posts`, запоминающий - запросы и с инъецируемым `respond(attempt, req) (status, body)`. Так проверяются - fallback-сценарии (первый запрос → 400, второй → 201). -- **WebSocket** — реальный `websocket.Upgrader` в `httptest` + `websocket.DefaultDialer`. - Единственный способ проверить, что `writeEvent` всегда шлёт `seq`. -- **Хендлер** — тестируется **структура напрямую** (`&mattermostInboundHandler{...}`), - без fx: так проверяются авторизация, префикс principal и nil-safety в изоляции. -- `fake...OwnerKVStore` + `recording...CommandIngress` — по образцу `zulip_ingress_test.go`. - -### 🔴 Дефект P1.1 — двойной префикс `principal`: починен в КОДЕ - -**Симптом (прод):** `/start owner=` в DM отвечает успехом, но `@balda` даёт -`settlement_stage="unauthorized"` навсегда. - -**Корень:** `auth.MattermostSubject(userID)` добавляет префикс -(`fmt.Sprintf("%s:%s", ChannelMattermost, ...)`), а `actors/command/start/handler.go:420` -вызывал её поверх `p.Principal`, который **уже** приходит префиксованным от -`mattermost_ingress.go:147` (`Principal: auth.MattermostSubject(cmd.SenderID)`). -Результат в БД — `mattermost:mattermost:`, а `GetUserByBinding` сравнивает **точно** → -совпадений нет → `unauthorized`. Плюс мусор в `display_name` и `created_by` channel-токенов. - -**Прошлый «фикс» был в данных** (прямой `UPDATE` в `state.db`) — и дефект **вернулся** при -следующей записи. 🔴 **Правило: дефект, найденный в данных, чинить в коде.** - -**Фикс (дельта `tools/deltas-mattermost-23-prefix-idempotent.json`):** хелпер сделан -**идемпотентным** — срезает уже существующий префикс: - -```go -// MattermostSubject returns the channel-qualified subject for a Mattermost user. -// It is idempotent: a value that is already channel-qualified ("mattermost:") -// yields the same subject, so callers may pass either a raw user ID or a subject -// without risking a doubled prefix ("mattermost:mattermost:"). -func MattermostSubject(userID string) string { - trimmed := strings.TrimSpace(userID) - trimmed = strings.TrimPrefix(trimmed, ChannelMattermost+":") - return fmt.Sprintf("%s:%s", ChannelMattermost, trimmed) -} -``` - -Из `userSubject` убран локальный `TrimPrefix`-костыль: -```go -case transportMattermost: - return auth.MattermostSubject(trimmed) -``` - -📌 **Почему фикс в хелпере, а не в вызывающем коде:** `MattermostSubject` вызывается из -трёх мест (`userSubject`, `mattermost_ingress.go:147`, `authorizeInbound`). Идемпотентность -закрывает **все** пути сразу и защищает от будущих вызовов поверх готового subject'а. -**Остальные вызовы были корректны** — дефект ровно один, но источник общий. - -**Регресс-тесты:** `TestMattermostSubjectIsIdempotent` (3 случая: raw, subject, padded) и -`TestMattermostHandlerSendsChannelQualifiedPrincipalExactlyOnce` (через публикуемую команду). - -### 🔴 Дефект P1.6 — `ProgressPolicy.Typing` расходился между путями - -`handlersfx/mattermost_ingress.go` в `HandleCommand` ставил -`ProgressPolicy{Typing: true, PlanUpdates: true}`, тогда как inbound-путь -(`inbound.go:135`) и `Adapter.SendTyping` (осознанный no-op — у ботов MM нет typing API) -говорили `false`. Мёртвый код: `SendProgress` при `Typing:true` вызывал `SendTyping`, -который всегда возвращал `nil`, и на каждом ходу писался бы warn только при ошибке. - -**Фикс** (дельта `tools/deltas-mattermost-24-typing.json`): `Typing: false` — command-путь -приведён к inbound-пути. **Оба пути теперь согласованы, и тест это фиксирует.** - -📌 **Урок:** при копировании Zulip-паттерна проверять каждую фичу на применимость к -провайдеру. Pan-путь и turn-путь обязаны давать **одинаковую** `ProgressPolicy` для одного -транспорта — расхождение невидимо компилятору. - -### 🔴 Второй урок: не доверять «проверил — зелёно» без проверки в изоляции - -Три моих первых теста падали, и **все три — моя ошибка, не дефект кода**: - -| Тест | Что я предположил | Реальность | -|---|---|---| -| `TestLocatorForPostScopesReplyToThreadRoot` | тред-сессия == сессия root-поста | Тред **отдельная** сессия от канала (`mm-t-...` vs `mm-c-...`) — это **правильный** дизайн, тест переписан на проверку «два ответа в треде → одна сессия» | -| `TestNewIngressFallsBackToClientUserID` | `NewIngress` подставляет bot_user_id из клиента | Не подставляет. Гард живёт в `onStart` («refuses to start without bot user id») — тест переписан на реальный контракт | -| `TestMattermostHandlerRejectsUnauthorizedCommand` | отказ возвращает `nil` | Возвращает `runtime is unavailable` (нет dispatcher для доставки отказа). **Главное — команда НЕ публикуется**, это и проверяется | - -📌 **Дыра в дизайне, найденная тестом:** root-пост треда (`RootID=""`) → сессия канала -`mm-c-`, а ответ в этом треде (`RootID=post-1`) → `mm-t-`. -То есть тред — **отдельная** беседа от канала. Это соответствует комментарию в -`locator_codec.go` («the locator key must stay stable for a whole thread»), но означает: -**бот, отвечающий в тред, ведёт отдельную от канала сессию.** Зафиксировано тестом -`TestLocatorForPostScopesReplyToThreadRoot` (два ответа в треде → одна сессия). - -⚠️ **Метод:** когда тест падает — сначала перечитать код и понять, чей это контракт. -Из трёх падений **ноль** оказались дефектами кода; все — неверные предположения теста. - - -🔴 **Ключевой урок:** `go build ./...` = exit 0 **не значит**, что транспорт рабочий. -fx-граф проверяется только `validateBaldaApplication` (или `fx.ValidateApp`). Компиляция -не видит ни `fx.In`-несовместимость, ни отсутствующие провайдеры. - -**Как воспроизвести проверку графа (эталон):** -```go -prepared, err := loadBaldaCommandConfig(true) // нужен os.Chdir в tempdir с .config/balda/config.yaml -err = validateBaldaApplication(prepared) // ← вот это и есть проверка графа -``` -Конфиг для теста — `mattermost.enabled: true` + `server_url`/`token`/`bot_user_id`/`bot_username`, -провайдер `balda_agent: opencode_acp`. Якорь `func TestValidateCommandRunsValidation(t *testing.T) {` -использован как точка вставки. - -### ✅ Текущее состояние — ЗАКРЫТО (2026-09-26, ночь) - -Оба ранее открытых пункта выполнены: - -1. ✅ **`TestBuiltinRoutes`** — в map `want` добавлен маршрут Mattermost - (`internal/apps/balda/deliveryfmt/registry_test.go`). Пакет `deliveryfmt` зелёный. -2. ✅ **`golangci-lint`** — запущен, 4 замечания → исправлены (см. §«Что дал golangci-lint» ниже). - -Дополнительно в этом же заходе: -- ✅ **`.go-arch-lint.yml` обновлён** — компоненты `mattermost_transport` / `mattermost_wiring`, - права зависимости у `composition_root` и `ingress_wiring`. Требование `AGENTS.md`. -- ✅ **Удалён мёртвый typing-throttle** целиком (не заглушен). -- ✅ Закоммичено: `004fda2f feat(mattermost): add Mattermost transport adapter` (40 файлов, +3072). - -**Ничего не осталось по коду.** Дальше — Фаза 3 (развернуть Балду с MM на Kraken, -провайдер DeepSeek) → [[personal/projects/balda/balda-mm-delegation-plan]]. - -### Что дал `golangci-lint` (4 замечания, все закрыты) - -| Замечание | Файл | Фикс | -|---|---|---| -| `inamedparam` ×2 | `ingress.go:452-453` (interface assertion) | `Start(ctx context.Context) error` / `Stop(ctx context.Context) error` | -| `unused` | `adapter.go:149` `shouldSendTyping` | удалён вместе со всем throttle-механизмом | -| `unused` | `ingress.go:21` `websocketReceiveTimeout` | константа удалена | -| `unlambda` | `mattermostfx/module.go:51` обёртка `func(client, logger) *Adapter` | заменена на голое `mattermost.NewAdapter` | - -**Корневая причина:** typing-throttle (`shouldSendTyping`, `typingThrottleInterval`, -`typingLastSentAt`, `typingMu`, `SetTypingThrottleInterval`) был скопирован из Zulip -по аналогии, но у MM-ботов **нет typing API** — `SendTyping` всегда `return nil`, значит -весь механизм недостижим. Удалён **целиком группой** (каскад: поле → setter → `sync` → -`zerolog` в `module.go`), а не заглушен `nolint`. - -📌 **Урок для будущих транспортов:** при копировании Zulip-адаптера проверять каждую фичу -на применимость к провайдеру. Перенос «по аналогии» создаёт мёртвый код, который линтер -находит только на финальном прогоне. Детали — [[personal/tech/balda-go-fx-pitfalls]]. - -### `.go-arch-lint.yml` — что добавлено - -Требование `AGENTS.md`: архитектурное изменение обязано обновлять arch-lint в том же коммите. -Без этого `go tool go-arch-lint check` падает двумя классами ошибок -(`composition_root shouldn't depend on …/mattermost`, `not attached to any component in archfile`). - -4 правки по образцу `zulip`: -- `components:` → `mattermost_transport` (`channel/mattermost` + `presentation/`) и - `mattermost_wiring` (`mattermostfx/`) -- `mayDependOn` для обоих новых компонентов -- `composition_root` → `+ mattermost_transport, mattermost_wiring` -- `ingress_wiring` → `+ mattermost_transport` - -Полные YAML-фрагменты — [[personal/tech/balda-go-fx-pitfalls]] §«go-arch-lint». - -### ✅ Финальные quality gates — все зелёные - -``` -go build ./... → exit 0 -go vet ./... → exit 0 -go tool golangci-lint run → 0 issues -go tool go-arch-lint check --project-path . → OK - No warnings found -тесты Mattermost → 3 PASS -``` - -### ✅ Предсуществующие падения тестов — НЕ наши - -13 пакетов падают **и на чистом `upstream/main`**. Проверено `git stash` (правки убраны → -тесты перезапущены → правки возвращены). Падающие: -`TestInitCommand_NonInteractiveAutoSelectsRootAndGeneratesDetectedAgents` (cmd/balda), -`TestMigrateSessionMemoryLayout_*` ×4 (state), `TestWorkspaceMCPImport/Export_*` (session), -`TestPluginResolverResolvesContainedArgvFilteredEnvAndLaunchSecrets` (mcpruntime), -`TestResolveWorkspaceBaseBranch_ConfigPreferredWhenValid`, -`TestCanonicalSessionMemory*` (apps/balda). -⚠️ Не пытаться «чинить» их как часть MM-задачи — это дефекты апстрима, отдельная работа. - -### Отклонения от плана разведки (важно) - -1. **`server.go` → `ingress.go`.** Планировался `server.go` по образцу Zulip, но у MM нет - HTTP-вебхука — это долгоживущее WS-соединение, а не `http.Server`. Имя `ingress.go` - точнее отражает суть. -2. **`SendDraftPlain` — не no-op.** В плане было «для MM это не no-op, edit-in-place есть». - По факту: `OperationDraft` **отправляется как обычный пост** (текст не теряется), а - edit-in-place живёт в `OperationClearQuestionControls` → `PUT /posts/{id}` — там он - семантически уместен (гашение кнопок после выбора). -3. **AddressKey не `t:`.** Планировалось `c:` / `dm:` / `t:`. По факту тред кодируется - **тем же channel-ключом с суффиксом**: `c:` (канал) и `c::` (тред). Причина: - тред MM — это root-post внутри канала, и локатор должен оставаться стабильным для всего треда. -4. **`ProgressPolicy{Typing: false}`.** Подтверждено: у MM-ботов **нет typing API** - (`SendTyping` возвращает `nil` осознанно — иначе шум на каждом ходу). -5. **`gorilla/websocket v1.5.3` был `// indirect`** в `go.mod` — стал прямым при сборке. -6. **`commandSupport` переименован** в экспортируемый `IngressCommandSupport` (нужен для `fx.In`), - оставлен алиас. - -### ✅ Импорт mattermostfx — ЗАКРЫТО - -`internal/apps/balda/app.go` — импорты добавлены (строки 27-28, рядом с `zulipfx`): -```go -"github.com/baldaworks/balda/internal/apps/balda/channel/mattermost" -"github.com/baldaworks/balda/internal/apps/balda/channel/mattermost/mattermostfx" -``` -Корневой `mattermost` нужен для `mattermost.ValidateConfig` в `validateMattermostConfig`. -`mattermostfx.Module` в списке модулей (строка ~667). `go build ./...` = exit 0. - -### 🔴 Что понадобилось дополнительно (не было в исходном коде) - -Первоначально модуль компилировался, но **DI-граф не собирался**. Три дефекта: - -1. **`fx.In` нельзя аннотировать внешним `fx.ParamTags`.** `IngressParams` содержит `fx.In`, - а в `module.go` стоял `fx.Annotate(NewIngress, fx.ParamTags(...))` → fx падает с - `fx.In structs cannot be annotated with fx.ParamTags or fx.From`. - **Фикс:** `name:`-теги перенесены **в поля `IngressParams`**, `NewIngress` без `fx.Annotate`. - Так же сделано в `zulipfx` (`zulip.NewServer` идёт голой ссылкой, теги в `ServerParams`). -2. **Не хватало провайдеров registered strings.** Модуль запрашивает - `balda_mattermost_{server_url,token,bot_user_id,bot_username}` — их никто не предоставлял. - **Фикс:** 4 `fx.Provide` в `app.go` (блок `// Mattermost transport`), значения берутся из - `cfg.Balda.Mattermost.*` через `strings.TrimSpace`. Для несуществующих ключей - `GetString` возвращает `""` — тот же паттерн, что у `balda_zulip_*`. -3. **Валидация стояла не там.** Функцию `validateMattermostConfig` добавили в `app.go` - (рядом с `validateSlackConfig`), вызов — после `validateSlackConfig` в `Module()`. - -**Метод, который это поймал:** `go build`/`go vet` этих ошибок **не видят** — только реальный -`fx.ValidateApp` через `validateBaldaApplication`. Для любого нового транспорта писать такой -тест сразу: он ловит неполный wiring, а не только синтаксис. - -### Конфиг, который нужен для запуска - -Ключи читаются через `fx.ParamTags` / `name:"…"`: - -``` -balda_mattermost_enabled -balda_mattermost_server_url -balda_mattermost_token -balda_mattermost_bot_user_id -balda_mattermost_bot_username -``` -Соответствие в `config.yaml` (по образцу `balda.zulip.*`): `balda.mattermost.{enabled,server_url,token,bot_user_id,bot_username}`. -Конкретные имена ключей yaml **проверить в `docs/reference/configuration.md`** перед Фазой 3 — -я их не сверял, только зафиксировал DI-теги. - -✅ **Проверено 2026-09-26:** env-имена — `BALDA_MATTERMOST_ENABLED`, `BALDA_MATTERMOST_SERVER_URL`, -`BALDA_MATTERMOST_TOKEN`, `BALDA_MATTERMOST_BOT_USER_ID`, `BALDA_MATTERMOST_BOT_USERNAME`. -Механизм: `appconfig/loader.go:367` делает `SetEnvPrefix("BALDA")` + -`SetEnvKeyReplacer(".", "_")` на leaf-ключах. **Env применяется только к ключам, которые уже есть -в YAML** — поэтому блок `balda.mattermost` обязан присутствовать в `cmd/balda/balda.yaml`, -иначе env-переменные молча игнорируются. Это же верно для любого нового транспорта. - -### Метод правок: дельта-апплаер (инструмент проекта) - -Правки проекта идут **не sed/не in-place**, а текстовыми дельтами. Причина: правило Alex -«никогда не использовать in-place скрипты и sed; писать скрипт, потом исполнять» + нужна -идемпотентность при повторных заходах. - -| Файл | Роль | -|---|---| -| `tools/apply-project-deltas.py` | движок: применяет JSON-дельты, ведёт состояние | -| `tools/run-project-deltas.sh` | обёртка: `check` / `apply` / `list` | -| `tools/deltas-mattermost.json` | основная группа (12 дельт: wiring + доки) | -| `tools/deltas-mattermost-2.json` | импорт корневого `mattermost` | -| `tools/deltas-mattermost-3.json` | тест env-override | -| `tools/deltas-mattermost-4.json` | тест fx-графа | -| `tools/deltas-mattermost-5.json` | фикс `fx.In` + `module.go` | -| `tools/deltas-mattermost-6.json` | 4 провайдера строк | -| `tools/deltas-mattermost-7.json` | маршрут Mattermost в `TestBuiltinRoutes` | -| `tools/deltas-mattermost-8.json` | `.go-arch-lint.yml`: `mattermost_transport`/`mattermost_wiring` | -| `tools/deltas-mattermost-9.json` | `inamedparam` + удаление `shouldSendTyping`/полей/`websocketReceiveTimeout` | -| `tools/deltas-mattermost-10.json` | удаление `SetTypingThrottleInterval` + `typingMu` + вызов/импорт `time` в `module.go` | -| `tools/deltas-mattermost-11.json` | удаление неиспользуемого импорта `sync` | -| `tools/deltas-mattermost-12.json` | `unlambda`: голое `mattermost.NewAdapter` | -| `tools/deltas-mattermost-13.json` | удаление неиспользуемого импорта `zerolog` из `module.go` | -| `tools/deltas-mattermost-23-prefix-idempotent.json` | 🔴 **P1.1:** `MattermostSubject` идемпотентна (срезает существующий префикс) + убран `TrimPrefix`-костыль из `userSubject` | -| `tools/deltas-mattermost-24-typing.json` | 🔴 **P1.6:** `ProgressPolicy.Typing` → `false` в command-пути (`handlersfx/mattermost_ingress.go`) | -| `tools/deltas-mattermost-25-docs-balda.json` | `docs/balda.md`: Mattermost в runtime stack, typing-нюанс, ссылка на гайд | -| `tools/deltas-mattermost-26-docs-readme.json` | `README.md`: Mattermost в 4 списках + `balda.mattermost.*` + ссылка на гайд | -| `tools/.deltas-state.json` | состояние (sha256 применённых якорей) | - -📌 **Дельты 9-13 — это один каскад линтера.** Удаление throttle-механизма тянуло -`SetTypingThrottleInterval` → `typingMu` → `sync` → `zerolog`. Пришлось делать 5 заходов, -каждый раз пересобирая. **Урок: удалять мёртвую фичу одной группой дельт**, а не по одному -симптому — тогда каскад импортов закрывается за раз. - -**Формат дельты:** -```json -{ "file": "путь/относительно/корня", "note": "что делаем", - "search": "уникальный фрагмент текущего текста", - "replace": "чем заменить" } -``` - -**Три состояния, которые различает апплаер:** -- `replace` уже есть в файле → `skipped` (идемпотентно, повторный прогон безопасен) -- `search` есть ровно один раз → `applied` -- ни `search`, ни `replace` → `error: ЯКОРЬ НЕ НАЙДЕН` (файл изменился не так, как ожидалось) - -**Команды:** -```bash -cd ~/Developer/balda -bash tools/run-project-deltas.sh check tools/deltas-mattermost.json tools/.deltas-state.json -bash tools/run-project-deltas.sh apply tools/deltas-mattermost.json tools/.deltas-state.json -bash tools/run-project-deltas.sh list tools/deltas-mattermost.json tools/.deltas-state.json -``` - -📌 `check` **обязателен перед `apply`** — он показывает TODO/MISSING/OK по каждому якорю. -Именно на этой проверке я поймал, что дельта валидации указывала в несуществующий -`validate()` в `config.go` (валидация живёт в `app.go`) и переписал её до записи. - -⚠️ **Не писать `search` с литеральными секретами** — значения собираются на хосте. -⚠️ Дельты с длинным `replace` писать **в JSON-файл через `write_file`**, не в heredoc: -кавычки и `\n` ломаются. - ---- - -## Объём работ - -Оценка из плана: **2-4 дня**. Разбивка: - -| Шаг | Объём | -|---|---| -| `client.go` (REST v4: posts, threads, files, reactions, websocket) | 1 день | -| `locator_codec.go` + `inbound.go` | 0.5 дня | -| `adapter.go` (все 6 операций + MM-специфика) | 1 день | -| `server.go` (WS ingress, reconnect, семафор, recover) | 1 день | -| `mattermostfx/` + presentation + prompt + structured | 0.5 дня | -| тесты (`go test -race ./...`) + `golangci-lint` | 0.5-1 день | - -### Quality gates (обязательны, из `AGENTS.md`) - -```bash -go test -race ./... -go tool golangci-lint run -``` - -### Логирование (правило проекта) - -Разрешено: `zerolog`, `log/slog`. Запрещено: `logrus`, `zap`, stdlib `log`. -Инициализация через `internal/logging.Init()`. Предпочитать структурированные поля. -Компонент логировать как `logger.With().Str("component", "balda.channel.mattermost").Logger()`. - -### Коммиты - -Conventional Commits. Синхронизация общих веток — **merge (`git pull --no-rebase`), не rebase**. - ---- - -## Что понадобится из Mattermost API - -| Задача | Endpoint | -|---|---| -| Отправка поста | `POST /api/v4/posts` (`{channel_id, message, root_id?, file_ids?}`) | -| Редактирование (draft→update) | `PUT /api/v4/posts/{post_id}` | -| Треды | `root_id` в create — ответы в тред | -| Файлы | `POST /api/v4/files` + `file_ids` в посте | -| Реакции (для question-контролов) | `POST /api/v4/reactions` | -| WebSocket события | `wss:///api/v4/websocket` (`posted`, `post_edited`) | -| Ping / health | `GET /api/v4/system/ping` | - -Авторизация: **Bot account + Personal Access Token** (`Authorization: Bearer `). -⚠️ Alex сказал: **бота создавать не надо** — токен предоставит он сам. - -## Открытые вопросы / решения Alex - -- **Бота не создавать** (2026-09-26, явно). Токен для MM-адаптера даст Alex. -- **Вопросы не задавать** — решения принимать самому, искать в Obsidian, а не спрашивать. -- **Команд, требующих подтверждения, не использовать** — писать скрипты. -- **Не останавливаться** до исполнения задачи. - -## Связанные заметки - -- [[personal/projects/balda/mattermost-kraken-deploy]] — сервер MM на Kraken (Фаза 1, ✅ закрыта) -- [[personal/projects/balda/balda-mm-delegation-plan]] — общий план -- [[personal/tech/balda-go-fx-pitfalls]] — 🔴 fx-питфоллы (fx.In/ParamTags, провайдеры) + метод дельт -- [[personal/projects/balda/balda-valera]] — текущая эксплуатация Балды/Валеры -- [[personal/tech/hermes-secret-redaction-bypass]] — как обходить маскировку секретов +Исторический дизайн и прежний PR-дневник перенесены в [[personal/projects/archive/balda-mattermost-connector-design-2026-09]]. Они не являются текущим runbook. diff --git a/personal/projects/personal-os/personal-os-status-2026-10.md b/personal/projects/personal-os/personal-os-status-2026-10.md index ef488120..ada76544 100755 --- a/personal/projects/personal-os/personal-os-status-2026-10.md +++ b/personal/projects/personal-os/personal-os-status-2026-10.md @@ -58,7 +58,7 @@ confidence: 1 - **Чат-транспорт:** Mattermost (сервер на Kraken: `mattermost` + `mm-postgres`). Zulip выводится из эксплуатации ([[personal/plans/zulip-decommission]]). Discord не используется с 2026-05-11. - **Hermes:** профиль `hermes-whale` (Кит) на Mac, Mattermost/Telegram. Профиль Eagle существует, кроны на паузе. -- **Balda / Execution Broker** (сверено 2026-10-14): текущий контур на Mac, Mattermost и Codex ACP; старый Kraken-стек демонтирован. Live coding flow на прежних образах прошёл с артефактами, публикацией результата и cleanup. Новый Broker rc.26 проверен локально, но согласованное обновление и production-приёмка кандидата не выполнены; универсальная подготовка стеков, caches и inputs между запусками остаются в плане. Подробности и ограничения: [[personal/projects/balda/execution-broker]], конфигурация: [[personal/projects/balda/setup]]. +- **Balda / Execution Broker** (сверено 2026-10-15): два инстанса Balda на Mac, Mattermost и Codex ACP; Broker rc.32 с отдельным approval hotfix. Развёртывание, реальная беседа с outage/restart, generic tooling, caches и artifact inputs проверены. Принятые provider-конфигурации и остающиеся границы Docker Codex, protected recovery и provider failure lifecycle перечислены в [[personal/projects/balda/execution-broker]]; общей release-ready приёмки ещё нет. Конфигурация: [[personal/projects/balda/setup]]. - **Virfield** (сверено 2026-10-14): Tart + SSH nodes, новая hardening-версия исправляет Lume migration и lifecycle defects; текущий кандидат ещё не live принят ([[personal/projects/virfield]]). - **Obsidian vault** `~/obsidian/` — жив, синхронизация через git/NAS + Syncthing; MCP — `mcpvault`. diff --git a/personal/tech/callee-prism.md b/personal/tech/callee-prism.md old mode 100755 new mode 100644 index d6ae83db..da8c27e4 --- a/personal/tech/callee-prism.md +++ b/personal/tech/callee-prism.md @@ -1,393 +1,13 @@ -# Callee + Prism — историческая справка по контрактам +# Callee / Prism — текущие контракты Broker -> [!WARNING] Версионная справка, 2026-09-29 -> Контракты ниже относятся к исследованным тогда версиям. Их нельзя считать описанием установленного Broker без сверки закреплённых зависимостей, workflow и worker package. -> Проверенные версии, текущие образы, реальные прогоны и оставшаяся работа: [[personal/projects/balda/execution-broker]]. -> Старый [[personal/plans/balda-callee-prism-plan|план интеграции]] сохранён как архив. +Проверено: **2026-10-15**. Работающие образы, принятые конфигурации и открытые границы: [[personal/projects/balda/execution-broker]]. План приёмки: [[personal/plans/balda-callee-prism-plan]]. -> Создан 2026-09-29. Опорная заметка к [[personal/plans/balda-callee-prism-plan]]. -> Назначение: справочник по контрактам callee и prism, чтобы не перечитывать -> доки апстрима при каждой правке ресурсов. +Callee исполняется внутри одноразового worker под управлением Runner. Broker закрепляет repository/revision, executor profile и SHA256 всего workflow root. Запрос модели не может подменить executable, credentials или неразрешённый workflow. Закреплённый upstream Callee — `57db8bb55733791bc01f273738dea5a095565ca0`; reviewed adapter хранится в `integrations/callee/` репозитория Broker. ---- +`Role` запускает выбранный ACP executor, `Script` выполняет операторскую команду, `Sequential` задаёт порядок. Authored permissions `deny/ask/allow` проверяет Callee. У managed OpenCode read-only роли используют native `plan` с запретом shell/effect tools и Callee deny; пишущие роли — `build` с утверждённой policy. Отказ модели выполнить запись сам по себе не доказывает permission enforcement. Отдельные тесты подают ACP-запросы записи в реальный handler и проверяют отказ и отсутствие файла. -## 1. Что это +Обычное owner approval и смена окружения имеют разные контракты. Environment checkpoint — opt-in операторский continuation: остановка и credential fence, snapshot, cleanup, отдельное одобрение, новая среда и исходный deadline. Произвольный live graph не сериализуется. Runtime/task не выбирает произвольный новый стек без allowlisted tooling profile. -| Инструмент | Репозиторий | Лицензия | Что делает | -|---|---|---|---| -| **callee** | `baldaworks/callee` | MIT | воркфлоу как версионируемые Markdown/YAML-ресурсы | -| **prism** | `baldaworks/prism` | MIT | плагин в coding agent: жизненный цикл Story/Epic | -| **prism-callee** | `baldaworks/prism-callee` | MIT | те же воркфлоу через callee-агентов | -| **Beads** | `gastownhall/beads` | — | durable-состояние (`bd`), носитель состояния prism | +Broker — источник истины о run, deadline, quality и cleanup. Prism adapter проецирует события в локальный Beads и сохраняет cursor. Успех Story подтверждается quality, SHA256 скачанных артефактов и `cleanup=done`; одно завершённое модельное сообщение этого не доказывает. Отправкой ответа в исходный чат владеет Balda с native durable waits. Публикация в GitHub, UI и другие прикладные действия принадлежат вызывающему проекту. -📌 Версия callee на 2026-09-29 — **`0.22.2`** (npm `@baldaworks/callee`, meta-пакет -с optionalDependencies на платформенные бинарники: `darwin-arm64`, `darwin-x64`, -`linux-arm64`, `linux-x64`, `win32-x64`). - ---- - -## 2. callee — контракты - -### 2.1 Типы узлов (10 публичных kinds, `callee.metalagman.dev/v1alpha1`) - -| Группа | Kind | Поведение | -|---|---|---| -| Листья | `Role` | промпт + один или несколько ходов в одной свежей ACP-сессии | -| | `DynamicRole` | то же, но конфиг провайдера рендерится из состояния визита | -| | `Script` | детерминированный локальный шаг через `sh`/`bash` | -| | `Human` | пауза, один непустой ответ оператора | -| Типизированные оценки | `TypeSafeJev` | нативный оценщик System One/Jev | -| | `OpenRouterDecision` | OpenRouter Decisions API | -| Композиты | `Sequential` | дети один раз, в порядке исходника | -| | `Parallel` | fan-out и join | -| | `Loop` | повтор детей в границах до эскалации | -| | `Router` | ровно один именованный или дефолтный ребёнок | - -### 2.2 Исполнение - -``` -Markdown/YAML - → decode + schema/semantic/template validation - → registry + static graph resolution - ⛔ отвергает: невалидные ресурсы, битые ссылки, циклы, - дубли resource ID, дубли effective ID в resolved tree - → resolved root tree + required parameter keys - → runner + ОДИН shared ephemeral state-объект - → ОДИН корневой артефакт в stdout (пишется ПОСЛЕ cleanup) -``` - -⚠️ Диагностика идёт в **stderr**, артефакт — в **stdout**. Непустой stderr **не** означает -провал. Успех определять по **exit status**. - -**Порядок визита узла:** счётчик → слияние `spec.state` со state входящего ребра → -рендер строковых листьев по **одному** pre-node снапшоту → атомарный коммит state- -модификатора → поведение по типу → `agent finished` со статусом и длительностью. - -🔴 При провале рендера state: **ни одна часть модификатора не коммитится, провайдер не стартует.** - -### 2.3 Провайдеры — ЗАКРЫТЫЙ список - -| `type` | Команда по умолчанию | Предусловие | -|---|---|---| -| `codex` | сам callee + `bridge codex` | авторизованный Codex CLI | -| `claude` | `npx -y @zed-industries/claude-code-acp@latest` | Node + npx + креды адаптера | -| `opencode` | `opencode acp` | OpenCode CLI | -| `copilot` | `copilot --acp --stdio` | Copilot CLI | -| `grok` | `grok agent stdio` | Grok CLI или `XAI_API_KEY` | -| `cursor` | `agent acp` | Cursor CLI | -| `generic_acp` | **нет — требует непустой `cmd`** | любой ACP-исполняемый файл | - -🔴 **Gemini не является валидным типом.** DeepSeek — тоже нет → только через `generic_acp`. - -**Поля провайдера:** - -| Поле | Контракт | -|---|---| -| `type` | обязателен, из закрытого списка | -| `cmd` | **одно имя исполняемого файла, НЕ shell-строка**; для `generic_acp` обязателен | -| `model` / `reasoning` / `mode` | опциональны; пустые → дефолт бэкенда | -| `extraArgs` | упорядоченный argv-список; каждый элемент непустой | -| `timeout` | Go duration, **по умолчанию `15m`** | - -⚠️ Конфиг провайдера обязан быть **вложенным** — плоские поля в `spec` не поддержаны. - -**Таймаут независим для:** старта процесса, создания и подготовки сессии, **каждого** -хода. Корневой прогон целиком он **не** ограничивает. - -**Переиспользование процесса:** одинаковые effective `type` + полностью резолвленный `cmd` -→ один процесс. `model`/`mode`/`reasoning` на идентичность процесса **не влияют**. -Сессия всегда создаётся свежая. - -### 2.4 DynamicRole — провайдер из состояния - -Все поля провайдера — шаблоны: `type`, `cmd`, `model`, `reasoning`, `mode`, `timeout`, -**каждый** элемент `extraArgs`. - -| Факт | Значение | -|---|---| -| Источники | `.Prompt`, текущий `.Input`, **один** согласованный post-node снапшот `.State`, `.Params` | -| Недоступно | `.Output`, env, файловая система, сеть, часы, рандом, криптография, мутаторы | -| Рендер | ровно **один раз на визит**; валидируется **до** поиска процесса | -| Периметр | конфиг фиксирован на весь визит, включая REPL-ходы | -| Повтор | новый визит Loop рендерит заново из актуального состояния | -| Ошибка | рендер или валидация падают → визит падает, ничего не стартует | - -📌 В `callee agent view` DynamicRole помечается `provider=runtime`; JSON-view сохраняет -авторские шаблоны. Обычный `callee doctor` валидирует ресурс, шаблон и граф, но готовность -провайдера **откладывает** — исполняемый файл проверяется только при достижении узла. - -📌 После рендера в lifecycle-событиях появляются `role_model` и `role_reasoning`. -**До** успешного рендера эти поля опускаются — их появление = рендер прошёл. - -🔴 **Безопасность:** state-driven `cmd` и `extraArgs` могут выбирать исполняемый код. -Не подавать туда недоверенный вывод модели без ограничения авторской шаблонной логикой. -`cmd` остаётся одним исполняемым файлом, `extraArgs` — argv-списком; shell не участвует. - -### 2.5 Script и Human - -**Script:** результат пишется в `State.scripts[effectiveId]` — `status` (`passed`/`failed`), -`exitCode` (`-1` при таймауте), `stdout`, `stderr`, `timedOut`. -🔴 **Таймаут всегда валит воркфлоу.** Ненулевой exit валит, если не задан `onNonZero: continue`. - -**Human:** рендерит тело, показывает на **управляющем терминале**, просит один непустой -ответ. Пишется в `State[responseKey]` и дублируется в `State.outputs[effectiveId]`. - -> 🔴 **Requires interactive mode** and an interactor backed by a controlling terminal. -> Explicit non-interactive mode **rejects a Human anywhere in the resolved tree during -> preflight, including beneath an unselected Router branch**. - -**Следствие:** воркфлоу с `Human` **не запускается** в non-interactive режиме вообще — -и проверка идёт до всякой логики, по всему resolved tree. Запуск через `docker exec` -без `-t` даёт non-interactive → `Human` невозможен. - -### 2.6 Эскалация - -Принадлежит **рёбрам**, а не определениям ресурсов. `Role` может эскалировать в ближайший -включающий `Loop` **только если каждое** ребро от Loop к этому вхождению имеет -`canEscalate: true` (опущенное = `false`). Вложенный `Loop` открывает **новую границу -авторизации** — потомки не наследуют полномочия внешнего. - -### 2.7 Команды - -```bash -npx --yes @baldaworks/callee@latest setup -callee agent list -callee agent view [--json] -callee agent run --message "..." [--repl-timeout D] -callee agent import --path --prefix [--force] -callee doctor -``` - ---- - -## 3. Prism — таск-трекинг - -### 3.1 Жизненные циклы - -**Story:** Specify → Design → Breakdown → **Human approval** → Apply → Verify. -Фазы: `phase:story:specify` → `design` → `breakdown` → `human` → `apply` → `verify`. - -**Epic:** Frame → Architecture → Roadmap → **approval** → Deliver Stories → Validation. -Фазы: `phase:epic:frame` → `architecture` → `roadmap` → `approval` → `delivery` → `validation`. - -### 3.2 Правила - -| Правило | Значение | -|---|---| -| Дети Epic | только **Stories** | -| Дети Story | только **Tasks** | -| Вложенные Epic, Task напрямую в Epic | **невалидно** | -| Фиксированный диапазон числа детей | **нет** — оценка качественная (покрытие, связность, ревьюимость, верифицируемость, ацикличность) | -| `Apply` | 🔴 **каждый переход требует явной авторизации человека** | -| Одобрение Epic → Story | **не переносится** | -| Критерии приёмки | показываются **только** по явному запросу оператора | - -При паузах prism останавливается только на: необходимом уточнении, одобрении перед -реализацией, блокерах. - -### 3.3 Установка и импорт - -**В coding agent** (независимая установка, например Codex): - -```bash -codex plugin marketplace add baldaworks/prism -codex plugin add prism@prism -# обновление: codex plugin marketplace upgrade prism → повторить add → новый thread -``` - -**Callee-агенты** (отдельный репозиторий, требует `callee >= 0.19.0` + `bd`): - -```bash -callee agent import baldaworks/prism-callee \ - --path pack/callee/prism \ - --prefix prism -# обновление/починка: тот же вызов + --force -``` - -**Проверка:** - -```bash -callee agent list | grep '^prism/' -callee agent view prism/lifecycle --json # 🔴 kind ДОЛЖЕН быть Router -callee agent view prism/story --json -callee agent view prism/epic --json -bd where -``` - -🔴 **`Sequential` вместо `Router` = устаревший импорт** → обновлять `--force`. -`--agent-root pack/callee` — только для поддержки самого репозитория. - -> «Each repository installs and validates independently — installing one plugin does -> not install the other.» - -### 3.4 Штатный и прямой запуск - -| Способ | Команда | Что даёт | -|---|---|---| -| Штатный (рекомендован) | `$prism-callee:lifecycle <запрос>` | резолвит Beads-цель, **персистит фазовые метки**, поддерживает resume | -| Прямой Story | `callee agent run prism/story --message "..."` | исполняет граф, но **не** резолвит Beads-элемент и не персистит метки | -| Прямой Epic | `callee agent run prism/epic --message "..."` | то же ограничение | - -📌 «The coding agent owns durable state and approval. Callee executes the selected graph; -**approval never transfers from an Epic to a Story.**» - ---- - -## 3.5 Роли: что это и как ими управляют - -**Роль — YAML/MD-файл в каталоге**, не право и не токен. Описание листа графа: провайдер, -промпт, параметры, таймаут. - -```yaml -apiVersion: callee.metalagman.dev/v1alpha1 -kind: Role -spec: - description: Собирает Xcode-проект - provider: - type: generic_acp - cmd: dsh-acp - model: deepseek-chat - timeout: 20m ---- -Собери проект {{ .Params.project }} и верни отчёт об ошибках. -``` - -| Источник ролей | Что даёт | -|---|---| -| Каталог по умолчанию | 6 стартовых ресурсов (`callee setup`) | -| Импорт из git | `callee agent import baldaworks/prism-callee --prefix prism` | -| Собственные | файлы в каталоге проекта или `~/.callee/` | - -🔴 **Ролей в рантайме не выдают.** Роль = наличие файла в каталоге. Рантайм-механизма -«выдать роль на задачу» **не существует**. - -### Что динамическое, а что нет - -| Сущность | Динамически? | Чем регулируется | -|---|---|---| -| Роль (файл графа) | ❌ статично | наличие файла в каталоге | -| Провайдер (`type`, `cmd`) | ❌ или из `State` через `DynamicRole` | автор графа | -| Файлы | ❌ | монтирование + рабочая копия | -| SSH-ключ | ❌ | наличие файла у процесса | -| **Разрешение модели внутри ACP-сессии** | ⚠️ **да** | `allow_all` / `ask` / `deny_all` | - -🔴 **Единственное, что реально запрашивается в рантайме — разрешение модели внутри -ACP-сессии** (`ask` → вопрос в чат). - ---- - -## 4. Права: формула и границы - -**`права = права процесса × смонтированное × разрешённое в графе`** - -| Уровень | Кто задаёт | -|---|---| -| Чем запущен процесс | вы, при старте (контейнер / хост-юзер / отдельный юзер) | -| Что смонтировано | compose | -| Что разрешено в графе | автор воркфлоу | - -🔴 **`permissions.mode: allow_all` — это про права модели ВНУТРИ ACP-сессии** (править -файлы, запускать команды). Оно **не** расширяет, что процесс видит в файловой системе. -Два разных ограничения. - -### Где callee — там и движки, и Script'ы - -🔴 **ACP-провайдер — дочерний процесс callee.** Движок на хосте + callee в контейнере -технически невозможны. Следствие: «callee на хосте» = **всё исполнение на хосте целиком**. - -### Гейт `Human` защищает действие, но не возможность - -Доступ у роли есть **всегда** — он определён тем, чем запущен callee и что смонтировано. -Апрув **не выдаёт доступ**, он разрешает выполнить операцию, которая уже технически -возможна. - -``` -Роль лежит в каталоге → доступ ЕСТЬ - ▼ -Узел Human («собрать Xcode в VM macos-27?») → пауза - ├─ апрув → Script исполняется (права у процесса УЖЕ были) - └─ отказ → граф останавливается -``` - -🔴 **Изоляцию даёт только то, чем процесс запущен и что смонтировано — не граф.** - -### Динамических маунтов не существует - -На macOS Docker маунты задаются **при создании контейнера**; live `mount --bind` -контейнер не видит; симлинк на внешний путь битый. **«Как модель запросит доступ к папке» -не решается через маунты.** - -| Подход | Оценка | -|---|---| -| A. Смонтировать всё заранее (`~/Developer:ro`) | запись нельзя | -| B. Пересоздание контейнера (`up -d`, ~3 с) | нужен механизм вне контейнера | -| C. Хост-агент ставит права сам | полный доступ, ноль изоляции | -| **D. Проект = рабочая копия** ✅ | модель работает в `~/agents/<проект>`, результат в проект через git | - -**Решение: D** — доступ задаётся наличием репозитория, а не маунтом. - -### Доступ выбирается маршрутом, а не запросом - -Модель **не просит** прав и **не может эскалировать**: нет доступа — задача падает. - -``` -Роутер в графе - ├─ задача требует ~/agents → роль в контейнере - ├─ задача требует Xcode/lume → роль на хост-исполнителе - └─ задача требует хоста по ssh → роль со ssh-ключом (command=) -``` - -**Разграничение = статический роутинг по типам задач**, не динамические права. - ---- - -## 5. DeepSeek как ACP-провайдер - -Провайдер callee — `generic_acp`, `cmd: dsh-acp`, пакет -**`@openma/deepseek-harness-acp@0.4.33`** (Apache-2.0, Node `>= 22.15`, -`bin: dsh-acp` → `dist/bin.js`). Обёртка вокруг официального DeepSeek Harness. - -| Способ передачи кред | Значение | -|---|---| -| Флаг | `api-key`, либо `api-key:` при нескольких провайдерах | -| Env | **`DEEPSEEK_API_KEY`** | -| Env | **`DEEPSEEK_BASE_URL`** — OpenAI-совместимый endpoint override | - -Приоритет: **флаги → переменные окружения → дефолты**. - -📌 `DEEPSEEK_BASE_URL` — OpenAI-совместимый override → смена провайдера сводится -к смене одной переменной, без правки графа. - -📌 Официальный `@deepseek-ai/dsh-acp` **не подошёл**: pre-release (последние версии -сплошь `-rc`/`-alpha`), `bin: null` — библиотека, а не запускаемый сервер. - ---- - -## 6. Питфоллы - -1. 🔴 **`Human` требует TTY** — non-interactive режим отвергает граф целиком, включая - невыбранные ветки Router. Запуск через `docker exec` без `-t` = non-interactive. -2. 🔴 **`cmd` — одно имя файла.** Аргументы только в `extraArgs`, иначе провайдер не стартует. -3. 🔴 **Не подавать вывод модели в `cmd`/`extraArgs`** — исполнение произвольного кода. -4. 🔴 **ACP-провайдер — дочерний процесс callee** — движок и callee не могут быть на - разных хостах. -5. 🔴 **Гейт `Human` не выдаёт доступ** — только разрешает уже возможное действие. -6. ⚠️ **`prism/lifecycle` обязан быть `Router`** — иначе устаревший импорт. -7. ⚠️ **Непустой stderr ≠ провал** — судить по exit status. -8. ⚠️ **Таймаут Script валит весь воркфлоу** — не «продолжает с ошибкой». -9. ⚠️ **Concurrency группы `Parallel`** используют один живой shared state. -10. ⚠️ **Поиск npm по `scope:normahq` / `scope:baldaworks` даёт пусто** — эти scoped-пакеты - через search-API не индексируются. Искать прямыми URL - `registry.npmjs.org/%2f`. -11. ⚠️ **Динамических маунтов на macOS Docker нет** — только пересоздание контейнера. - ---- - -## Связанные заметки - -- [[personal/plans/balda-callee-prism-plan]] — план внедрения (топология: стек на Mac) -- [[personal/projects/balda/balda-mm-delegation-plan]] — общий план Фаз 1-5 -- [[personal/plans/balda-mm-phase4-agents]] — Фаза 4 (мост, приёмка) -- [[personal/tech/agents-bridge]] — мост :8790 (демонтируется в новой топологии) -- [[personal/tech/balda-executor-mcp]] — MCP-сервер делегирования (демонтируется) -- [[personal/tech/macos-docker-virtiofs-mount-limits]] — почему нет динамических маунтов +Команды и схемы: `docs/protocol.md`, `docs/executors.md`, `docs/environment-changes.md`, `docs/operations.md` Broker. Версионная справка сентября сохранена в [[personal/projects/archive/callee-prism-contracts-2026-09-29]] и не используется как актуальный API reference.