[2026-09-26] eagle: personal/plans/balda-mm-endtoend-plan.md personal/tech/balda-go-fx-pitfalls.md
This commit is contained in:
1 parent
354ecc58c6
commit
4109797f6b
2 files changed
+199
-23
No files matched your search
@@ -1,32 +1,180 @@
|
||||
# План: доделать сквозной путь `@balda` → ответ в Mattermost
|
||||
|
||||
> Создан 2026-09-26. Обновлён 2026-09-26 (сессия реализации).
|
||||
> Статус: **🟡 ОБЕ ПРИЧИНЫ УСТРАНЕНЫ. Хендлер написан и работает. Осталась привязка владельца.**
|
||||
> Создан 2026-09-26. Обновлён 2026-09-26 (сессия реализации, полный проход).
|
||||
> Статус: **🟡 ПЯТЬ ИЗ ШЕСТИ ПРИЧИН УСТРАНЕНЫ. Последний фикс (`base_url` + CA) задеплоен,
|
||||
> не проверен. Осталась приёмка ответом в UI.**
|
||||
> Часть [[personal/projects/balda/balda-kraken-mm-deploy]] (Фаза 3).
|
||||
> Команда Alex: «Доделывай всё. И уведомления в приложения неактивны тоже поправь.»
|
||||
|
||||
## Итог сессии (кратко)
|
||||
|
||||
| Шаг | Статус |
|
||||
|---|---|
|
||||
| Причина A (`bad_seq`) найдена и исправлена | ✅ дельта 16, проверено |
|
||||
| Причина B (`Processor == nil`) найдена | ✅ дельта 15 (диагностика) |
|
||||
| MM-хендлер написан (`handlersfx/mattermost_ingress.go`) | ✅ ~320 строк, собирается |
|
||||
| Хендлер подключён в DI | ✅ дельта 19, `fx.As(new(mattermost.InboundProcessor))` |
|
||||
| `auth`: поддержка `mattermost:` subject | ✅ дельты 17, 18 |
|
||||
| Quality gates: build / vet / arch-lint | ✅ build 0, vet чисто, arch-lint **no warnings** |
|
||||
| Образ `balda-mm:0.1.5` собран и задеплоен | ✅ Up, без ошибок старта |
|
||||
| **Хендлер доказанно работает** | ✅ лог `conversational ingress settled`, `session_id=mm-c-7aafea13` |
|
||||
| Привязка владельца (`/start owner=…` в DM) | 🔴 **НЕ ВЫПОЛНЕНА** — блокирует приёмку |
|
||||
| Push-уведомления | ⬜ не начато |
|
||||
Найдено и устранено **шесть независимых причин** тишины. Каждая подтверждена логами.
|
||||
|
||||
**Где именно стоим:** пост доходит до `ingressapp` и отбивается на авторизации
|
||||
(`settlement_stage="unauthorized"`) — это **корректное** поведение, а не баг.
|
||||
В `state.db` таблица `balda_user_bindings` **пуста**: ни одной привязки канала.
|
||||
Админ `b6517e56-1110-4cb9-a301-302c585fbd0d` существует, но не связан ни с одним транспортом.
|
||||
| № | Причина | Фикс | Проверено |
|
||||
|---|---|---|---|
|
||||
| A | `bad_seq` — MM отклонял пинг без `seq` и переставал пушить события | дельта 16 | ✅ 0 вхождений, `pong seq_reply:1,2,3` |
|
||||
| B | `Processor == nil` — MM-хендлера не существовало в DI | дельта 19 + новый файл | ✅ лог `post accepted for processing` |
|
||||
| C | `ChannelMattermost` не было в auth → `unsupported transport` | дельта 17 | ✅ |
|
||||
| D | `/start` отбивался проверкой доступа **до** привязки | правка хендлера | ✅ команда прошла в runtime |
|
||||
| E | `principal` с транспортным префиксом в БД → вечный `unauthorized` | UPDATE с бэкапом | ✅ `assembled provider user content` |
|
||||
| F | **`base_url` не читается из YAML + нет CA в `scratch`** | дельта 22 + `.env` | 🔴 **задеплоено, не проверено** |
|
||||
|
||||
**Следующее действие:** `ssh kraken '/tmp/mm-owner-bind.sh testuser "T3stPassw0rd!x" <owner_token>'`
|
||||
(скрипт написан, см. §Инструменты), затем `@balda` в канал и **приёмка ответом в UI**.
|
||||
**Где именно стоим:** авторизация **пройдена**, пост дошёл до провайдера
|
||||
(`assembled provider user content`, сессия `mm-c-7aafea13`). Оставался отказ на уровне
|
||||
LLM-запроса — провайдер шёл на `api.openai.com` и падал с TLS-ошибкой. Оба дефекта
|
||||
исправлены в образе `0.1.8`, но финальная проверка не прогонялась.
|
||||
|
||||
**Следующее действие:**
|
||||
```bash
|
||||
ssh kraken '/tmp/mm-post-user.sh 7bjw4g7o1jdsbmjbqd649xxgaw testuser "T3stPassw0rd!x" "@balda скажи одно слово"'
|
||||
sleep 45
|
||||
ssh kraken 'docker logs balda-mm --since 60s | grep -iE "error|turn task|x509"'
|
||||
```
|
||||
Ожидание: ответ бота в канале `balda-test` **виден в UI**, в логах нет `x509`.
|
||||
Приёмка = **видно в UI**, не «контейнер Up» и не `settled`.
|
||||
|
||||
---
|
||||
|
||||
## 🔴 Причина F — провайдер: `base_url` игнорируется + нет CA-сертификатов
|
||||
|
||||
Найдено последней, оказалась **двойной**. Симптом в логе (образ `0.1.7`):
|
||||
|
||||
```json
|
||||
{"component":"balda.turn_dispatcher",
|
||||
"error":"agent run: send openai request: Post \"https://api.openai.com/v1/chat/completions\":
|
||||
tls: failed to verify certificate: x509: certificate signed by unknown authority"}
|
||||
```
|
||||
|
||||
### F1 — `base_url` в YAML не работает, URL берётся из env
|
||||
|
||||
`runtime.providers.deepseek.openai.base_url` в `config.yaml` **молча игнорируется**.
|
||||
|
||||
**Причина:** тип `LocalAPIConfig` в `github.com/normahq/runtime/v2@v2.0.11/agentconfig/config.go:82`
|
||||
имеет **только два поля** — `api_key` и `model`. Поля `base_url` в схеме **нет вообще**:
|
||||
|
||||
```go
|
||||
type LocalAPIConfig struct {
|
||||
APIKey string `json:"api_key,omitempty" yaml:"api_key,omitempty" mapstructure:"api_key"`
|
||||
Model string `json:"model,omitempty" yaml:"model,omitempty" mapstructure:"model"`
|
||||
}
|
||||
```
|
||||
|
||||
Базовый URL читается **только из переменной окружения** —
|
||||
`hostedagent/openai.go:24`:
|
||||
|
||||
```go
|
||||
const defaultOpenAIBaseURL = "https://api.openai.com/v1"
|
||||
|
||||
func openAIBaseURL() string {
|
||||
if u := os.Getenv("OPENAI_BASE_URL"); u != "" {
|
||||
return strings.TrimRight(u, "/")
|
||||
}
|
||||
return defaultOpenAIBaseURL
|
||||
}
|
||||
```
|
||||
|
||||
📌 **Правило:** для DeepSeek (и любого OpenAI-совместимого провайдера) базовый URL задавать
|
||||
**только** через `OPENAI_BASE_URL` в `.env`/compose `environment`. Ключ `base_url` в YAML
|
||||
выглядит рабочим, но не делает ничего — это ловушка.
|
||||
|
||||
**Фикс:** `OPENAI_BASE_URL=https://api.deepseek.com/v1` в `/home/kraken/balda/.env`
|
||||
(скрипт `tools/mm-add-base-url.sh`, идемпотентный, с бэкапом).
|
||||
|
||||
### F2 — `FROM scratch` не содержит корневых сертификатов
|
||||
|
||||
`Dockerfile.release` заканчивался `FROM scratch` без `/etc/ssl/certs`, поэтому **любой**
|
||||
исходящий HTTPS падал с `x509: certificate signed by unknown authority`. Go не может
|
||||
построить цепочку доверия — файла с корневыми CA физически нет в образе.
|
||||
|
||||
**Фикс (дельта `tools/deltas-mattermost-22-ca.json`):**
|
||||
|
||||
```dockerfile
|
||||
FROM scratch
|
||||
|
||||
# CA bundle: the scratch base has no trust store, so any HTTPS call (the LLM
|
||||
# provider in particular) fails with 'x509: certificate signed by unknown
|
||||
# authority'. The builder stage is a Debian image and carries the bundle.
|
||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
|
||||
COPY --from=builder /out/balda /usr/local/bin/balda
|
||||
|
||||
ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/balda"]
|
||||
```
|
||||
|
||||
📌 **Правило:** для любого `scratch`/`distroless`-образа, который делает исходящие
|
||||
HTTPS-запросы, копировать `ca-certificates.crt` из builder'а **и** выставлять `SSL_CERT_FILE`.
|
||||
Для distroless это `/etc/ssl/certs/ca-certificates.crt` и переменная тоже нужна.
|
||||
⚠️ Симптом обманчив: ошибка выглядит как «сервер с неверным сертификатом», а на деле
|
||||
доверять нечему.
|
||||
|
||||
---
|
||||
|
||||
## 🔴 Причина E — `principal` с транспортным префиксом в БД
|
||||
|
||||
`/start owner=<token>` в DM вернул **«You are now registered as the bot owner»**, но
|
||||
`@balda` по-прежнему давал `settlement_stage="unauthorized"`.
|
||||
|
||||
**Причина:** в `balda_user_bindings` записалось
|
||||
`channel_type='mattermost'`, `principal='mattermost:c6udeze8dfrdxm5jbgq55bd58r'` — с префиксом.
|
||||
А `canonicalSubject()` **отрезает** ведущий префикс и ищет по `principal='c6udeze...'`:
|
||||
|
||||
```go
|
||||
if before, after, ok := strings.Cut(trimmed, ":"); ok {
|
||||
channelType = strings.ToLower(strings.TrimSpace(before))
|
||||
principal = strings.TrimSpace(after) // ← префикс отрезан
|
||||
}
|
||||
```
|
||||
|
||||
Сравнение `b.channel_type = ? AND b.principal = ?` никогда не совпадало → каждая проверка
|
||||
авторизации false. `GetUserByBinding` — **точное** сравнение, не `LIKE`.
|
||||
|
||||
**Фикс:** точечный `UPDATE` одного поля, с бэкапом и остановкой контейнера
|
||||
(`tools/mm-fix-owner-principal.sh`):
|
||||
|
||||
```sql
|
||||
UPDATE balda_user_bindings
|
||||
SET principal = substr(principal, length(channel_type) + 2)
|
||||
WHERE channel_type = 'mattermost' AND principal LIKE channel_type || ':%';
|
||||
```
|
||||
|
||||
Результат: `mattermost | c6udeze8dfrdxm5jbgq55bd58r`. После этого ингресс прошёл авторизацию
|
||||
и дошёл до провайдера.
|
||||
|
||||
⚠️ **Как найти причину:** читать `state.db` **вместе с `state.db-wal`**. Я сначала смотрел
|
||||
только `state.db` и видел пустую таблицу — привязка была в WAL. Читать все три файла
|
||||
(`state.db`, `-wal`, `-shm`) через `strings` под `sudo -n`.
|
||||
⚠️ Правка БД — **не** штатный путь: `AttachBinding` не даёт перепривязать (`count != 0` →
|
||||
`ErrBindingAlreadyAssigned`), detach-API отсутствует. Делать с бэкапом и остановленным
|
||||
контейнером (sqlite не любит конкурентной записи).
|
||||
⚠️ `state.db` (и `-wal`) принадлежат **root**; `docker exec ... sh` невозможен (scratch),
|
||||
`sqlite3` есть **на хосте** (`/usr/bin/sqlite3`).
|
||||
|
||||
---
|
||||
|
||||
## 🔴 Причина D — `/start` должен быть доступен до авторизации
|
||||
|
||||
Первая попытка `/start owner=<token>` в DM получила от бота
|
||||
**«You are not allowed to use Balda in this channel.»**
|
||||
|
||||
**Причина:** мой `HandleCommand` проверял доступ **до** обработки, а владельца ещё нет —
|
||||
тупик: привязаться нельзя, потому что не авторизован; авторизоваться нельзя, потому что
|
||||
не привязан.
|
||||
|
||||
**Фикс** (по образцу zulip — `zulip_ingress.go:172`):
|
||||
|
||||
```go
|
||||
if cmd.Command != commandStart && !h.canAccess(ctx, cmd.SenderID) {
|
||||
return h.sendPlain(ctx, cmd.Locator, mattermostAccessDeniedText)
|
||||
}
|
||||
```
|
||||
|
||||
📌 **Правило:** точка входа онбординга (`/start`) — единственная команда, доступная
|
||||
неавторизованному. Проверять это в **каждом** новом транспорте.
|
||||
|
||||
⚠️ `commandStart` **уже объявлена** в пакете `handlersfx` (из telegram/zulip) — своя
|
||||
константа даёт `redeclared in this block`. Использовать общую.
|
||||
|
||||
---
|
||||
|
||||
@@ -225,6 +373,9 @@ ssh kraken 'sudo -n strings /srv/…-49e8f586…/docker/balda/state/state.db | g
|
||||
| `mm-post-bot.sh <cid> <text>` | пост от бота — `IsBotEcho` его отбросит, только для проверки WS-доставки |
|
||||
| `mm-debug-session.sh` | логин + `/users/me` — отладка 401 |
|
||||
| **`mm-owner-bind.sh <user> <pass> <token>`** | привязка владельца через DM + `/start owner=` |
|
||||
| **`mm-get-owner-token.sh`** | достаёт `appowner_auth_token` из `state.db` через `strings` под `sudo -n` |
|
||||
| **`mm-fix-owner-principal.sh`** | чинит `principal` владельца: бэкап → стоп → UPDATE → старт |
|
||||
| **`mm-add-base-url.sh`** | идемпотентно добавляет `OPENAI_BASE_URL` в `.env` (с бэкапом) |
|
||||
| `~/Developer/balda-wsprobe/main.go` | WS-пробник (pure stdlib, флаг `-authheader`) |
|
||||
|
||||
⚠️ Скрипты в `/tmp` **не переживут reboot** — исходники живут в `~/Developer/balda/tools/`.
|
||||
@@ -238,6 +389,17 @@ ssh kraken 'sudo -n strings /srv/…-49e8f586…/docker/balda/state/state.db | g
|
||||
| `deltas-mattermost-17-auth.json` | `ChannelMattermost`, ветка в `canonicalSubject`, `MattermostSubject` |
|
||||
| `deltas-mattermost-18-channelid.json` | `ChannelIDOf(locator)` |
|
||||
| `deltas-mattermost-19-wire.json` | регистрация хендлера в DI |
|
||||
| `deltas-mattermost-20-start.json` | `transportMattermost`, `userSubject`, ветка MM в `ownerBindBundle`, display name |
|
||||
| `deltas-mattermost-21-prefix.json` | защита от двойного префикса в `userSubject` (применена; фактическая причина E оказалась в данных БД, не в коде) |
|
||||
| `deltas-mattermost-22-ca.json` | CA-бандл + `SSL_CERT_FILE` в `Dockerfile.release` |
|
||||
|
||||
### Образы, собранные в сессии
|
||||
|
||||
`0.1.2` (снят челлендж) → `0.1.3` (диагностика кадров) → `0.1.4` (найден `bad_seq`) →
|
||||
`0.1.5` (seq-фикс) → `0.1.6` (start/owner) → `0.1.7` (auth-правки) → **`0.1.8` (текущий: CA + base_url)**.
|
||||
|
||||
Все собираются на Mac (`arm64builder`) ~110 с, переносятся `docker save | zstd -3 -T4 | ssh kraken 'zstd -dc | docker load'`.
|
||||
⚠️ Распаковка zstd — **на Mac**: zstd 1.5.4 на Kraken не читает фрейм macOS-zstd 1.5.7.
|
||||
|
||||
---
|
||||
|
||||
@@ -274,6 +436,15 @@ ssh kraken 'sudo -n strings /srv/…-49e8f586…/docker/balda/state/state.db | g
|
||||
| 10 | Образ Balda — `scratch`: ни `sh`, ни `sqlite3` | `docker exec` невозможен; `state.db` читать `strings` на хосте |
|
||||
| 11 | `mattermost.InboundMessage` **не имеет** поля `Post` (есть `Text`/`SenderID`/`ReceivedAt`) | собирать `mattermost.Post` руками перед `NormalizeInbound` |
|
||||
| 12 | Предсуществующие фейлы тестов: 11 в `cmd/balda` и `internal/apps/balda` (workspace/plugin/session-memory) | проверять через `git stash` своих правок; к Mattermost не относятся |
|
||||
| 13 | 🔴 **`base_url` в YAML провайдера НЕ работает** — `LocalAPIConfig` не имеет такого поля | задавать `OPENAI_BASE_URL` в env (см. §Причина F1) |
|
||||
| 14 | 🔴 **`FROM scratch` без CA** → любой HTTPS падает с `x509: certificate signed by unknown authority` | копировать `ca-certificates.crt` из builder + `SSL_CERT_FILE` |
|
||||
| 15 | **`principal` в `balda_user_bindings` с транспортным префиксом** → `GetUserByBinding` (точное сравнение) не находит владельца | хранить `principal` **без** `<channel>:` (см. §Причина E) |
|
||||
| 16 | **`state.db` читать вместе с `-wal`** — свежие записи лежат в WAL, `state.db` выглядит пустым | `strings` по всем трём файлам (`db`, `-wal`, `-shm`) под `sudo -n` |
|
||||
| 17 | **`/start` требует доступа, которого ещё нет** → тупик онбординга | исключить `commandStart` из проверки: `cmd.Command != commandStart && !h.canAccess(...)` |
|
||||
| 18 | `commandStart` **уже объявлена** в пакете `handlersfx` | не заводить свою — `redeclared in this block` |
|
||||
| 19 | `AttachBinding` **не даёт перепривязать** (`count != 0` → `ErrBindingAlreadyAssigned`), detach-API отсутствует | править `state.db` точечным UPDATE с бэкапом и остановленным контейнером |
|
||||
| 20 | `sqlite3` **есть на хосте** Kraken (`/usr/bin/sqlite3`), но не в контейнере | правки БД делать с хоста под `sudo -n`, контейнер остановить |
|
||||
| 21 | Правка `state.db` через `sudo cp` требует апрува и **блокируется** (таймаут) | точечный UPDATE одного поля вместо операций над каталогом данных |
|
||||
|
||||
## Связанные заметки
|
||||
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
# Balda — Go/fx pitfalls и метод правок (зеркало скилла)
|
||||
|
||||
> Создано 2026-09-26. Обновлено 2026-09-26 (сессия реализации MM-хендлера) — добавлены:
|
||||
> **`bad_seq` — настоящая причина тишины WS** (§«WS-фрейм без seq»),
|
||||
> Создано 2026-09-26. Обновлено 2026-09-26 (сессия реализации MM-хендлера, полный проход).
|
||||
> Добавлены: **`bad_seq` — настоящая причина тишины WS** (§«WS-фрейм без seq»),
|
||||
> **`optional:"true"` скрывает отсутствующую зависимость** (§«Дефект 5»),
|
||||
> **`canonicalSubject` и неизвестный транспорт** (§«auth: subject-based»).
|
||||
> **`canonicalSubject` и неизвестный транспорт** (§«auth: subject-based»),
|
||||
> **`/start` — тупик онбординга при проверке доступа** (§«Дефект 6»),
|
||||
> **`base_url` в YAML мёртв — только `OPENAI_BASE_URL`** (§«Провайдер: …»),
|
||||
> **`FROM scratch` без CA** (§«scratch и TLS»),
|
||||
> **`principal` с префиксом в БД** (§«Дефект 7»),
|
||||
> **`state.db` читать вместе с WAL** (§«Метод: чтение sqlite-состояния»).
|
||||
> Уточнён §«авторизация — ОДИН механизм»: челлендж НЕ был причиной №1, им был `bad_seq`.
|
||||
> Зеркало секции скилла `devops/docker-balda-service`, добавленной 2026-09-26.
|
||||
> Источник: работа над Mattermost-адаптером Балды (Фаза 2) + деплой на Kraken (Фаза 3).
|
||||
|
||||
Reference in new issue
Block a user