[2026-09-26] eagle: personal/projects/balda/balda-mattermost-connector.md personal/projects/balda/balda-mm-delegation-plan.md

This commit is contained in:
Alexey Martemyanov committed 2026-09-26 02:23:48 +06:00
1 parent 88782f6ad7
commit 47ff9bfacd
2 files changed
+143 -48

No files matched your search

@@ -149,24 +149,40 @@ MM: `Source` = новый `turncmd.SourceMattermost`, `DeliveryFormat` = `Delive
(3 метода: `ProcessInbound`, `HandleCommand`, `HandleUnsupportedCommand`),
`SupportedCommands() []string`, `commandSupported(name)`.
### `server.go` — ingress
### `ingress.go` — ingress (фактически реализовано)
⚠️ **Zulip использует outgoing webhook по HTTP.** У MM для ботов — **WebSocket**
(`wss://<host>/api/v4/websocket`, `Sec-WebSocket-Protocol: bearer <token>`, порт 443/8065).
Это отличается от Zulip-образца принципиально — нужен WS-клиент с reconnect.
(`wss://<host>/api/v4/websocket`). Это отличается от Zulip-образца принципиально.
Обязательно перенести из Zulip-сервера:
- **семафор на конкурентную обработку** (`processSem chan struct{}`, Zulip — 16) + `WaitGroup`;
при заполнении отвечать «busy»/дропать, а не копить;
- **`defer recover()`** вокруг обработки сообщения → не ронять процесс;
- **таймаут обработки** (`zulipWebhookProcessingTimeout` = 5 мин) через
`context.WithTimeout(context.WithoutCancel(...))`;
- **фильтр эхо-сообщений бота** (`IsBotEcho`) — иначе бесконечный цикл;
- **не превышать лимит тела** (1 MiB) — для MM это актуально для file-attachments;
- graceful `Stop()` с ожиданием активной обработки (`waitForWebhookProcessing`);
- все HTTP-таймауты заданы явно (`ReadHeaderTimeout`/`ReadTimeout`/`WriteTimeout`/`IdleTimeout`);
**Как реализована аутентификация (проверено компиляцией):**
- dial с заголовком `Authorization: Bearer <token>`;
- **плюс** обязательный первый кадр `{"seq":1,"action":"authentication_challenge","data":{"token":"<token>"}}` —
без него MM не начинает слать события;
- `WebSocketURL()` в `client.go` сам конвертирует `http→ws` / `https→wss` и добавляет `/api/v4/websocket`.
⚠️ В ранних заметках было `Sec-WebSocket-Protocol: bearer <token>` — **это не то, что реализовано
и не то, что описано в актуальных доках 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)` —
@@ -206,6 +222,80 @@ MM: `Source` = новый `turncmd.SourceMattermost`, `DeliveryFormat` = `Delive
---
## Что фактически написано (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:<ch>` / `c:<ch>:<root>` / `d:<ch>`**. `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) |
### Отклонения от плана разведки (важно)
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:<root_id>`.** Планировалось `c:` / `dm:` / `t:`. По факту тред кодируется
**тем же channel-ключом с суффиксом**: `c:<ch>` (канал) и `c:<ch>:<root>` (тред). Причина:
тред 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`),
оставлен алиас.
### 🔴 Известный незакрытый шаг
`internal/apps/balda/app.go` — модуль уже в списке, но **импорт не добавлен**:
```go
"github.com/baldaworks/balda/internal/apps/balda/channel/mattermost/mattermostfx"
```
Вставить рядом с `zulipfx` (строки 27). LSP: `undefined: mattermostfx` на строке 667.
**Без этого `go build ./...` не пройдёт** — проверять первым делом.
### Конфиг, который нужен для запуска
Ключи читаются через `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-теги.
---
## Объём работ
Оценка из плана: **2-4 дня**. Разбивка:
@@ -181,41 +181,37 @@ Mattermost (Alex) — итог + артефакты
2. Сохранить стэш `whale-20260623` в отдельную ветку (не в stash — стэши теряются).
3. Снять текущий compose Валерии/Клавдия в бэкап (`~/Docker/balda-agent/`, `~/Docker/claudio-agent/`).
### Фаза 1 — Mattermost (Kraken) — 🔄 В РАБОТЕ
### Фаза 1 — Mattermost (Kraken) — ✅ СЕРВЕР РАБОТАЕТ
Детальный док: [[personal/projects/balda/mattermost-kraken-deploy]].
1. ✅ Лицензия — self-hosted **Team Edition** (free). Ограничения по числу пользователей
смотреть на момент установки.
2. 🔄 Развернуть MM на Kraken — **БЛОКЕР НАЙДЕН И РЕШЁН**: официальный образ
`mattermost/mattermost-team-edition` существует **только для amd64**, Kraken = arm64.
Официальной поддержки arm64 у Mattermost нет (KB: «not officially supported for production»).
**Решение:** собираем свой образ из **официального arm64-бинаря**
`releases.mattermost.com/11.11.1/mattermost-team-11.11.1-linux-arm64.tar.gz`.
Compose и `.env` на Kraken записаны; сборка запущена.
3. ✅ Порт выбран: **8065** (был свободен). Тома — на USB-HDD (`49e8f586…/docker/mattermost`),
НЕ на SD (иначе SD забьётся — инцидент 2026-06-24).
4. ⛔ **Бота НЕ создавать** — решение Alex 2026-09-26. Токен даст он сам.
5. ⏳ Создать команду + канал для агентов после подъёма.
6. ⏳ Cloudflare hostname **`mm.qentra.top` → HTTP `localhost:8065`** (делает Alex),
затем `MM_SERVICESETTINGS_SITEURL` → `https://mm.qentra.top`.
1. ✅ Лицензия — self-hosted **Team Edition** (free).
2. ✅ Развернуть MM на Kraken. **Блокер arm64 найден и решён:** официальный образ
`mattermost/mattermost-team-edition` — **только amd64**, официальной поддержки arm64 нет.
**Решение:** свой образ из официального arm64-бинаря `releases.mattermost.com`.
🔴 **Сборка — НА MAC, не на Kraken** (Alex, 2026-09-26): Mac — тот же arm64 и в 10× быстрее
(33 с против 346+ с), а buildkit-кэш на Kraken льётся на **SD-карту** (не на data-root).
Результат: `health=healthy restarts=0`, API `{"status":"OK"}` с Kraken и с Mac по LAN.
3. ✅ Порт **8065**; тома на USB-HDD (`49e8f586…/docker/mattermost`), НЕ на SD.
4. ⛔ **Бота НЕ создавать** — 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`, затем приёмка входом в браузере.
### Фаза 2 — адаптер Mattermost в Балде — 🔄 РАЗВЕДКА ЗАВЕРШЕНА
### Фаза 2 — адаптер Mattermost в Балде — ✅ КОД НАПИСАН, ⏳ ТЕСТЫ
Детальный док: [[personal/projects/balda/balda-mattermost-connector]].
1. ✅ Вместо новой директории `~/Developer/balda-mm` — работаем в `~/Developer/balda`
на ветке **`feat/mattermost-channel`**, созданной от **чистого `upstream/main`**
(`de273d8f`). Это эквивалентно «чистому клону», но без второго рабочего дерева.
2. ✅ Изучен `channel/zulip/*` целиком: adapter (331), client (289), server (316),
inbound (192), locator_codec (229), presentation, `zulipfx/` (module/prompt/structured).
3. ⏳ Написать `channel/mattermost/*` по контракту `transport-presentation-boundary.md`:
- ingress: **WebSocket** `wss://<host>/api/v4/websocket` (не outgoing webhook!);
- delivery: рендеринг Markdown MM, треды через `root_id`, файлы через `file_ids`,
edit-in-place через `PUT /posts/{id}` (в отличие от Zulip, где draft — no-op);
- 7 групп DI, как в `zulipfx/module.go`.
4. ⏳ Shared-слой: `ChannelTypeMattermost`, `TransportMattermost`, `NameMattermostMarkdown`,
`turncmd.SourceMattermost`, колонка в `docs/commands.md`.
⚠️ `channel/router.go` **не** правится — он map-based.
5. ⏳ Quality gates: `go test -race ./...` + `go tool golangci-lint run`.
1. ✅ Ветка **`feat/mattermost-channel`** от чистого `upstream/main` (`de273d8f`).
2. ✅ Изучен `channel/zulip/*` целиком как образец.
3. ✅ **Написан пакет `channel/mattermost/*`** — 12 файлов, ~2200 строк: client, locator_codec,
inbound, adapter (все 9 операций), media, ingress (WebSocket), formatting, presentation,
`mattermostfx/` (4 файла, 7 групп DI).
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`.
### Фаза 3 — оркестратор на Kraken (~1-2 дня)
1. Развернуть Балду с MM-адаптером на Kraken, провайдер — **DeepSeek** (через API-ключ, не подписка).
@@ -244,11 +240,11 @@ Mattermost (Alex) — итог + артефакты
| Бан подписочного аккаунта за «unusual traffic» | средний | только официальный бинарь; не более 1-2 параллельных задач; не проксировать токен; мониторить письма Anthropic |
| MM-адаптер разъедется с апстримом | высокий | держать как отдельный fork-патч, ребейзить на каждый релиз; либо предложить в апстрим PR |
| Kraken: USB-HDD монтируется ~130 с, есть mount-race | известный | drop-in `RequiresMountsFor` уже применён и проверен reboot'ом; volume не на data-root |
| RPi5 не тянет сборку Балды | средний | собирать образ на Mac arm64, на Kraken только `pull`/`up` |
| Утечка секретов в MM-канал | средний | `balda.permissions.mode: ask`; redaction в job-events уже есть в апстриме |
| Mattermost free-лицензия ограничивает нужное | низкий | проверить лимиты до установки |
| 🔴 **Официального arm64-образа MM НЕ существует** | **подтверждён** | свой образ из официального arm64-бинаря; сторонние community-образы не брать. См. [[personal/projects/balda/mattermost-kraken-deploy]] |
| 🔴 **Hermes маскирует секреты в tool-выводе** | **подтверждён** | `write_file` не редактируется; значения не писать литералами — собирать на хосте. См. [[personal/tech/hermes-secret-redaction-bypass]] |
| RPi5 не тянет сборку | **подтверждён** | 🔴 **собирать ЛЮБЫЕ образы для Kraken на Mac, НИКОГДА на Kraken** — buildkit-кэш льётся на SD, плюс 10× медленнее. Перенос `docker save │ zstd` → `ssh kraken 'docker load'`. См. [[personal/projects/balda/mattermost-kraken-deploy]] |
| Mattermost free-лицензия ограничивает нужное | низкий | Team Edition работает; проверено на 11.11.1 |
| 🔴 **Официального arm64-образа MM НЕ существует** | **решён** | свой образ из официального arm64-бинаря; сторонние community-образы не брать. См. [[personal/projects/balda/mattermost-kraken-deploy]] |
| 🔴 **Hermes маскирует секреты в tool-выводе** | **решён (обход найден)** | `write_file` пишет байт-в-байт и **не** редактируется; маскировка только на ЧТЕНИИ (`code_file=True`). Значения не писать литералами — собирать на хосте. См. [[personal/tech/hermes-secret-redaction-bypass]] |
| 🔴 **uid bind-mount конфликт на Kraken** | **подтверждён (2×)** | владелец тома обязан совпадать с uid из образа. `postgres/`→`70:70`, остальное→`2000:2000`. Тот же класс, что crash-loop `hermes-kraken` |
---
@@ -292,6 +288,15 @@ Mattermost (Alex) — итог + артефакты
→ [[personal/projects/balda/balda-mattermost-connector]]
- Побочно: разобран механизм маскировки секретов Hermes и найден обход.
→ [[personal/tech/hermes-secret-redaction-bypass]]
- **2026-09-26 (позже)**: **Фаза 1 ЗАКРЫТА, Фаза 2 — код написан.**
- Alex потребовал **закрытую регистрацию** в MM — сделано и проверено живым запросом (HTTP 501).
- Alex указал на ошибку: **сборка на Kraken вместо Mac** → перенесена на Mac (33 с против 346+ с),
buildkit-кэш на SD больше не растёт. Урок зафиксирован в §Риски.
- MM на Kraken: `health=healthy restarts=0`, API OK с Kraken и Mac, админ создан.
- Коннектор: 12 файлов написаны, `go build ./...` = exit 0.
- Найдены и починены 3 дефекта деплоя: `CMD ["mattermost","server"]`, `--strip-components` в tar,
двойной uid-конфликт (`postgres/` vs остальное).
- 🔴 Незакрыто: импорт `mattermostfx` в `app.go` + тесты коннектора.
## Связанные заметки
- [[personal/projects/balda/mattermost-kraken-deploy]] — Фаза 1: MM на Kraken (arm64-блокер, деплой)