[2026-09-26] eagle: personal/projects/balda/balda-mattermost-connector.md personal/projects/balda/balda-mm-delegation-plan.md personal/projects/balda/mattermost-kraken-deploy.md personal/tech/balda-go-fx-pitfalls.md

This commit is contained in:
Alexey Martemyanov committed 2026-09-26 10:44:19 +06:00
1 parent 47ff9bfacd
commit 06950241dd
4 files changed
+442 -13

No files matched your search

@@ -1,15 +1,25 @@
# Balda — Mattermost коннектор (архитектура)
> Создано 2026-09-26. Обновлено 2026-09-26. Статус: **КОД НАПИСАН, КОМПИЛИРУЕТСЯ**.
> Создано 2026-09-26. Обновлено 2026-09-26 (вечер). Статус: **КОД СОБИРАЕТСЯ, DI-ГРАФ ВАЛИДИРУЕТСЯ**.
> Часть проекта [[personal/projects/balda/balda-mm-delegation-plan]] (Фаза 2).
> Требование Alex: поддержка **всех** функций Балды, по новым API, production-качество.
>
> **Проверено:** `go build ./...` → exit 0, `go vet` чист. Тесты ещё не написаны.
> Осталось: **добавить импорт `mattermostfx` в `app.go`** (LSP подсветил, не успел),
> затем тесты + `go test -race ./...` + `golangci-lint`.
> **Проверено (2026-09-26):**
> - `go build ./...` → exit 0, `go vet ./...` → exit 0
> - **DI-граф собирается целиком** с `balda.mattermost.enabled=true` — проверено реальным
> `validateBaldaApplication(prepared)` через fx, а не только компиляцией
> - 3 новых теста зелёные (см. §«Тесты»)
>
> **Осталось:** 2 шага — дописать ожидание маршрута Mattermost в `TestBuiltinRoutes`
> (единственная регрессия от наших правок) + `go tool golangci-lint run`.
> Подробности в §«🔴 Текущее состояние и следующий шаг».
>
> Разделы ниже — сначала контракт/разведка, затем **фактически написанные файлы**. Ищи
> §«Что фактически написано» для реестра файлов.
>
> 🔧 **Инструмент правок:** `tools/apply-project-deltas.py` + `tools/run-project-deltas.sh` +
> `tools/deltas-mattermost*.json` — правки проекта идут **текстовыми дельтами с якорями**,
> не sed/in-place. См. §«Метод правок: дельта-апплаер».
---
@@ -252,6 +262,124 @@ MM: `Source` = новый `turncmd.SourceMattermost`, `DeliveryFormat` = `Delive
| `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` |
🔴 **Ключевой урок:** `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) {`
использован как точка вставки.
### 🔴 Текущее состояние и следующий шаг
**Осталось 2 шага (~5 минут):**
1. **Дописать ожидание маршрута Mattermost в `TestBuiltinRoutes`.**
`internal/apps/balda/deliveryfmt/registry_test.go`, функция `TestBuiltinRoutes`.
В map `want` добавить:
```go
{transport: TransportMattermost, deliveryFormat: DeliveryFormatNone}: NamePlainText,
```
Симптом без правки: `len(BuiltinRoutes()) = 5, want 4`.
Причина: в `routes.go` мы добавили маршрут Mattermost (строка 32), а тест-ожидание не обновили.
**Это единственная регрессия, внесённая нашими правками.**
2. `go tool golangci-lint run` — ещё не запускался.
### ✅ Предсуществующие падения тестов — НЕ наши
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 нет
@@ -294,6 +422,59 @@ balda_mattermost_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-state.json` | состояние (sha256 применённых якорей) |
**Формат дельты:**
```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` ломаются.
---
## Объём работ
@@ -352,7 +533,8 @@ Conventional Commits. Синхронизация общих веток — **mer
## Связанные заметки
- [[personal/projects/balda/mattermost-kraken-deploy]] — сервер MM на Kraken
- [[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]] — как обходить маскировку секретов
@@ -196,9 +196,12 @@ Mattermost (Alex) — итог + артефакты
5. ✅ Админ `admin@qentra.top` создан (пароль в `.adminpw`).
6. ✅ **Регистрация закрыта** — проверено живым POST → HTTP 501 `signup_email_disabled`.
7. ✅ CF-маршрут `mm → localhost:8065` добавлен Alex'ом.
8. ⏳ `MM_SERVICESETTINGS_SITEURL` → `https://mm.qentra.top`, затем приёмка входом в браузере.
8. ✅ **`SITEURL` → `https://mm.qentra.top` — сделано 2026-09-26**: бэкап compose → правка
скриптом (не sed) → `docker compose up -d` → `healthy restarts=0` (~45 с) → ping через
туннель `{"status":"OK"}`. ⏳ Осталась приёмка входом в браузере (делает Alex).
→ [[personal/projects/balda/mattermost-kraken-deploy]]
### Фаза 2 — адаптер Mattermost в Балде — ✅ КОД НАПИСАН, ⏳ ТЕСТЫ
### Фаза 2 — адаптер Mattermost в Балде — ✅ КОД СОБИРАЕТСЯ, DI-ГРАФ ВАЛИДИРУЕТСЯ
Детальный док: [[personal/projects/balda/balda-mattermost-connector]].
1. ✅ Ветка **`feat/mattermost-channel`** от чистого `upstream/main` (`de273d8f`).
@@ -209,9 +212,20 @@ Mattermost (Alex) — итог + артефакты
4. ✅ Shared-слой: `ChannelTypeMattermost`, `TransportMattermost`, `NameMattermostMarkdown`,
`turncmd.SourceMattermost`, `mattermostfx.Module` в `app.go`.
⚠️ `channel/router.go` **не** правился — он map-based (подтверждено).
5. ✅ `go build ./...` = exit 0, `go vet` чист.
6. 🔴 **Незакрыто:** импорт `mattermostfx` в `app.go` (LSP: `undefined: mattermostfx`).
7. ⏳ Тесты по образцу `zulip/*_test.go` + `go test -race ./...` + `golangci-lint run`.
5. ✅ `go build ./...` = exit 0, `go vet ./...` = exit 0.
6. ✅ **Импорт `mattermostfx` + корневого `mattermost` в `app.go` добавлен** (блокер снят).
7. ✅ **Wiring закрыт:** 5 fx-провайдеров `balda_mattermost_*`, `MattermostConfig` + валидация,
дефолты в `balda.yaml`, доки `configuration.md`/`commands.md`, проверка канала в `command.go`.
8. ✅ **DI-граф проверен реальным `validateBaldaApplication`** с `mattermost.enabled=true` —
найдены и починены 3 дефекта (`fx.In` + `fx.ParamTags`, отсутствующие провайдеры строк,
отсутствующий импорт).
9. ✅ 2 новых теста зелёные (env-override + fx-граф).
10. ⏳ **Осталось:** дописать ожидание маршрута Mattermost в `TestBuiltinRoutes`
(`deliveryfmt/registry_test.go`) + `go tool golangci-lint run`.
🔴 **Урок Фазы 2:** `go build` = exit 0 **не значит** «транспорт рабочий». fx-несовместимости
и отсутствующие провайдеры видны только при `fx.ValidateApp`/`validateBaldaApplication`.
13 падающих пакетов в апстриме — **предсуществующие**, проверено `git stash`, не наши.
### Фаза 3 — оркестратор на Kraken (~1-2 дня)
1. Развернуть Балду с MM-адаптером на Kraken, провайдер — **DeepSeek** (через API-ключ, не подписка).
@@ -297,10 +311,29 @@ Mattermost (Alex) — итог + артефакты
- Найдены и починены 3 дефекта деплоя: `CMD ["mattermost","server"]`, `--strip-components` в tar,
двойной uid-конфликт (`postgres/` vs остальное).
- 🔴 Незакрыто: импорт `mattermostfx` в `app.go` + тесты коннектора.
- **2026-09-26 (вечер)**: **Фаза 1 закрыта полностью, Фаза 2 — блокер сборки снят.**
- `mm.qentra.top` подтверждён Alex'ом как работающий. `SITEURL` → `https://mm.qentra.top`
(бэкап compose → скрипт `set-siteurl.py` → `compose up -d` → `healthy restarts=0` →
ping через туннель OK).
- Коннектор: импорт `mattermostfx` **и** корневого `mattermost` в `app.go` добавлен →
`go build ./...` = exit 0.
- Закрыт весь wiring: `MattermostConfig` + `validateMattermostConfig`, 5 fx-провайдеров
`balda_mattermost_*`, дефолты в `balda.yaml`, доки `configuration.md` + `commands.md`,
Mattermost в `validateBaldaChannelConfiguration`.
- 🔴 **Внедрён метод правок через дельта-апплаер** (`tools/apply-project-deltas.py`) —
идемпотентные текстовые дельты с якорями вместо sed/in-place.
→ [[personal/projects/balda/balda-mattermost-connector]] §«Метод правок»
- **Тесты поймали 3 дефекта, которых не видел `go build`:** `fx.In` + внешний
`fx.ParamTags`, отсутствующие провайдеры зарегистрированных строк, отсутствующий импорт.
Проверка графа — реальный `validateBaldaApplication`, не компиляция.
- Внесена 1 регрессия: `TestBuiltinRoutes` ждёт 4 маршрута, в `BuiltinRoutes()` их 5.
Фикс известен (дописать строку в map `want`), не применён — лимит шагов сессии.
- ⚠️ 13 падающих пакетов — **предсуществующий дефект апстрима**, подтверждено `git stash`.
## Связанные заметки
- [[personal/projects/balda/mattermost-kraken-deploy]] — Фаза 1: MM на Kraken (arm64-блокер, деплой)
- [[personal/projects/balda/balda-mattermost-connector]] — Фаза 2: архитектура MM-коннектора
- [[personal/tech/balda-go-fx-pitfalls]] — 🔴 fx-питфоллы + метод правок дельтами
- [[personal/tech/hermes-secret-redaction-bypass]] — обход маскировки секретов Hermes
- [[personal/tech/docker-data-root-mount-race]] — mount-race на Kraken (закрыт, reboot-безопасен)
- [[personal/projects/balda/balda-valera]] — текущая эксплуатация Балды/Валеры (форк, старая архитектура)
@@ -5,8 +5,9 @@
> админ создан, **регистрация закрыта** (проверено живым запросом → HTTP 501).
> Часть проекта [[personal/projects/balda/balda-mm-delegation-plan]].
>
> **Осталось:** Alex добавил маршрут `mm → 8065` в CF-туннель (подтвердил 2026-09-26).
> Нужно: `SITEURL` → `https://mm.qentra.top` + приёмка входом в браузере.
> **Осталось:** Alex подтвердил, что `mm.qentra.top` работает (2026-09-26).
> **Сделано:** `SITEURL` → `https://mm.qentra.top`, MM перезапущен, проверено через туннель.
> **Осталось:** приёмка входом в браузере (делает Alex).
---
@@ -297,9 +298,33 @@ ssh kraken "docker ps --format '{{.Names}}\t{{.Status}}' | grep -E 'mattermost|m
| **Регистрация закрыта** | ✅ **HTTP 501 на самописную регистрацию (живая проверка)** |
| `config.json` = рантайм | ✅ приведены к согласию (`EnableUserCreation/OpeNserver: false`) |
| Cloudflare `mm → 8065` | ✅ **добавил Alex 2026-09-26** («up») |
| `SITEURL` → `https://mm.qentra.top` | ⏳ **следующий шаг** |
| `SITEURL` → `https://mm.qentra.top` | ✅ **сделано 2026-09-26** (бэкап → скрипт → `compose up -d` → проверка) |
| Ping через туннель `https://mm.qentra.top/api/v4/system/ping` | ✅ `{"status":"OK"}` |
| `mmctl config get ServiceSettings.SiteURL` | ✅ `"https://mm.qentra.top"` |
| Приёмка входом в браузере | ⏳ **делает Alex** |
### Как менялся SITEURL (2026-09-26, выполнено)
Порядок по правилу «коммит ДО → правка → заливка → проверка»:
1. **Бэкап compose** на хосте: `cp docker-compose.yml backups/docker-compose.yml.bak-$(date +%Y%m%d-%H%M%S)`
→ `/home/kraken/mattermost/backups/docker-compose.yml.bak-20260926-103937`.
2. **Правка скриптом, не sed.** `/tmp/mm-deploy/set-siteurl.py` (локально на Mac) →
`ssh kraken 'cat > /tmp/set-siteurl.py' < set-siteurl.py` → `ssh kraken 'python3 /tmp/set-siteurl.py'`.
Скрипт идемпотентен (NOOP при совпадении), пишет во временный файл и делает атомарный `replace`.
Вывод: `OK: MM_SERVICESETTINGS_SITEURL: "http://192.168.1.14:8065" -> MM_SERVICESETTINGS_SITEURL: "https://mm.qentra.top"`.
3. **Перезапуск:** `ssh kraken 'cd /home/kraken/mattermost && docker compose up -d mattermost'`
→ `mattermost Recreated → Started`, `mm-postgres Healthy`.
4. **Ожидание health** (`docker inspect mattermost --format '{{.State.Health.Status}} restarts={{.RestartCount}}'`):
`starting` ×8 итераций по 5 с → **`healthy restarts=0`** на 9-й (~45 с).
5. **Проверка с Mac:** `curl -s https://mm.qentra.top/api/v4/system/ping` → `{"status":"OK",...}`;
`docker exec mattermost mmctl --local config get ServiceSettings.SiteURL` → `"https://mm.qentra.top"`.
⚠️ `sed -i` из старой инструкции ниже **не использовать** — он оставлен как история. Правка
compose — только скриптом с бэкапом.
⚠️ При пересборке compose из генератора SITEURL перезапишется на LAN-значение. Долгосрочно —
менять значение в генераторе, а не в файле.
### Команды для следующего шага
```bash