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

This commit is contained in:
Alexey Martemyanov committed 2026-09-26 14:01:35 +06:00
1 parent f9b55abb8c
commit aefc2f9095
3 files changed
+318 -30

No files matched your search

+130 -4
View File
@@ -1,14 +1,135 @@
# План: доделать сквозной путь `@balda` → ответ в Mattermost
> Создан 2026-09-26. Обновлён 2026-09-26 (сессия реализации, полный проход).
> Статус: **✅ ЗАКРЫТ И ПРИНЯТ. Все шесть причин устранены, шестая проверена — бот отвечает
> в UI. Push-уведомления: 🔴 невозможны на Team Edition (разбор ниже) — задача снята с плана.**
> Статус: **🔴 НЕ ЗАКРЫТ. «Приёмка пройдена» была ложной — сквозной путь проверялся от
> ТЕСТОВОГО аккаунта `testuser`, а не от аккаунта Alex (`admin`). Alex: «Нихуя он мне не
> отвечает в ММ. Не готово!!» — ПОДТВЕРДИЛОСЬ логами. Владельцем бота привязан `testuser`,
> аккаунт Alex в `balda_user_bindings` отсутствует полностью → его посты отбиваются
> `settlement_stage=unauthorized`. См. §«ПОЧЕМУ НЕ РАБОТАЕТ У ALEX» ниже — это причина №7.**
> Push-уведомления: 🔴 невозможны на Team Edition (разбор ниже) — задача снята с плана.
> Часть [[personal/projects/balda/balda-kraken-mm-deploy]] (Фаза 3).
> Команда Alex: «Доделывай всё. И уведомления в приложения неактивны тоже поправь.»
## ✅ ПРИЁМКА ПРОЙДЕНА (2026-09-26)
## 🔴 ПОЧЕМУ НЕ РАБОТАЕТ У ALEX — причина №7 (главная, найдена 2026-09-26 13:5x)
Сквозной путь работает. В канале `balda-test` (`7bjw4g7o1jdsbmjbqd649xxgaw`):
Alex написал `@balda привет` в `balda-test` — бот молчит. Лог:
```json
{"component":"balda.handlersfx.mattermost","session_id":"mm-c-7aafea13",
"inbound_id":"mattermost:dqagqce7pibiipt3qtqnkgfd8c",
"settlement_outcome":"terminal","settlement_stage":"unauthorized",
"message":"conversational ingress settled"}
```
**Причина:** в `balda_user_bindings` единственная строка — владелец привязан к
`principal='c6udeze8dfrdxm5jbgq55bd58r'`, это **`testuser`** (аккаунт, которым я тестировал).
Аккаунта Alex — `admin`, MM user_id `cbwsorzwoigbtrdnteugnx4aqo` — в таблице **нет вообще**.
```
cbwsorzwoigbtrdnteugnx4aqo | admin | system_admin ← Alex, НЕ привязан
c6udeze8dfrdxm5jbgq55bd58r | testuser | system_user ← привязан как владелец (мой тест)
edxrgupoxiyg5gzmr79tjypbme | balda | system_user ← бот
```
`IsOwnerSubject` ищет строго по паре `(channel_type='mattermost', principal=<mm_user_id>)`.
Пары для Alex нет → каждая проверка авторизации false → `unauthorized`.
📌 **Мой доклад «фаза закрыта» был неверным.** Я проверял путь от `testuser`, чей post
прошёл авторизацию, и записал это как приёмку. Реальный пользователь — Alex — не проходил.
🔴 **Урок приёмки: проверять ОТ ИМЕНИ ЗАКАЗЧИКА.** Если владелец — Alex, тест обязан
идти с его MM user_id. Чужой аккаунт в bindings делает сквозной путь «рабочим» только
для тестового пользователя — это ложная приёмка.
### Второй симптом (не дефект): пост без `@balda` отбрасывается намеренно
```
{"component":"balda.channel.mattermost.ingress","post_id":"jznhm9ks6jfrtdze5r5qa7g6ic",
"bot_username":"balda","message":"Привет","message":"dropping posted event: bot not mentioned"}
```
Это **корректное** поведение (`MentionsBot`). В канале обращаться надо `@balda <текст>`.
### 🔴 Причина №7 — второй дефект: двойной префикс жив в КОДЕ, не только в БД
`actors/command/start/handler.go:420`:
```go
case transportMattermost:
return auth.MattermostSubject(strings.TrimPrefix(trimmed, transportMattermost+":"))
```
`MattermostSubject()` (`auth/owner.go:307`) **сама** добавляет префикс:
```go
func MattermostSubject(userID string) string {
return fmt.Sprintf("%s:%s", ChannelMattermost, strings.TrimSpace(userID))
}
```
А `trimmed` = `p.Principal` приходит **уже с префиксом** от MM-хендлера
(`mattermost_ingress.go:147`: `Principal: auth.MattermostSubject(cmd.SenderID)`).
⚠️ **`TrimPrefix` здесь снимает префикс, но только один раз, и это не помогает:**
хендлер положил `mattermost:c6udeze…`, `TrimPrefix` даёт `c6udeze…`, `MattermostSubject`
снова даёт `mattermost:c6udeze…` — верно. Но **исходный `p.Principal` уже содержал
префикс**, потому что в прошлой сессии `MattermostSubject` вызывался дважды на разных слоях.
Результат в БД — `mattermost:mattermost:c6udeze…` (виден в `display_name` и в `created_by`
channel-токенов). В прошлой сессии я починил **строку в БД**, но **не код** — поэтому
дефект воспроизвёлся снова при создании токенов.
🔴 **Правило: чинить дефект в коде, а не в данных.** Правка БД без правки кода = дефект
вернётся при следующей записи. Проверять оба слоя.
### 🔴 Блокер привязки Alex — замкнутый круг (нужно решение)
| Путь | Почему не работает |
|---|---|
| `/start owner=<owner_auth_token>` | `HasOwner()` = `len(OwnerSubjects()) != 0`. Владелец (`testuser`) есть → идёт в `handleExistingOwnerState` → `IsOwner(Alex)` = false → **«Bot owner is already registered»** |
| `/start <channel_token>` для MM | `CreateMissingOwnerBindTokens` пропускает канал, где `HasOwnerSubject(channel)` = true. MM считается привязанным (см. строку `testuser`) → **токен для MM не создаётся вообще** |
| `registerOwner` → `attachCanonicalBinding` | Ищет кандидата: `Role == administrator` **и `Binding == nil`**. Единственный админ `b6517e56-…` **уже имеет binding** → `no unbound active administrator user` |
**Выход (выбран, ожидает апрува Alex):** перевести существующий binding с `testuser`
на аккаунт Alex одним `UPDATE` — пользователь `b6517e56-…`, роль и `HasOwner()` остаются,
меняется только `principal` + `display_name`. Это **минимальное** вмешательство: не `DELETE`,
обратимо обратным `UPDATE`, и не требует ни правки кода, ни пересборки.
```sql
UPDATE balda_user_bindings
SET principal = 'cbwsorzwoigbtrdnteugnx4aqo',
display_name = 'mattermost:cbwsorzwoigbtrdnteugnx4aqo'
WHERE channel_type = 'mattermost'
AND principal = 'c6udeze8dfrdxm5jbgq55bd58r';
```
Перед этим — бэкап `state.db` с `-wal` (обязательно).
### Факты БД (2026-09-26, актуальные)
```
balda_users: 1 строка — b6517e56-1110-4cb9-a301-302c585fbd0d / admin / administrator
balda_user_bindings: 1 строка — mattermost | c6udeze8dfrdxm5jbgq55bd58r (testuser)
display_name = mattermost:mattermost:c6udeze8… ← мусор, двойной префикс
balda_collaborators: пусто
balda_user_binding_claims: 1 строка (consumed)
balda_app_kv: owner_auth_token = XDucUE… (32 симв.)
channel_token:* → telemetry: telegram, slackagent, zulip
🔴 для mattermost токена НЕТ (канал считается привязанным)
```
⚠️ `owner_auth_token` в доке **не хранить** — только способ получения:
`sqlite3 "$DB" "SELECT value_json FROM balda_app_kv WHERE key='owner_auth_token';"`
(БД на хосте: `/srv/…-6194539b…/docker/balda/state/state.db`, читать вместе с `-wal`).
### Текущее состояние блокера
**`@balda` от Alex → `unauthorized`, ответа нет.** Бот, транспорт, провайдер, сессии,
доставка — все исправны (доказано проходом от `testuser`). Не хватает **одной строки
привязки** для аккаунта Alex. Работа остановлена на апруве `UPDATE` в БД.
---
## ⚠️ ПРЕДЫДУЩИЙ БЛОК «ПРИЁМКА ПРОЙДЕНА» — НЕПОЛНАЯ (проверено от `testuser`)
Сквозной путь работал **только для аккаунта `testuser`**. В канале `balda-test` (`7bjw4g7o1jdsbmjbqd649xxgaw`):
```
c6udeze8dfrdxm5jbgq55bd58r (testuser) | @balda reply with a single word
@@ -513,6 +634,11 @@ DiagnosticId kgib6ormjf8sf8xds7dkhaee5c
| 24 | `psql -t -c "… name IN ('DiagnosticId')"` в ssh-строке ломается на кавычках → `column "DiagnosticId" does not exist` | экранировать `'"'"'` внутри ssh-команды, либо использовать `ILIKE` без кавычек на значении |
| 25 | Приёмка «виден в UI» без GUI-браузера | `GET /api/v4/channels/{id}/posts?per_page=N` + `jq` по `user_id` автора — однозначное доказательство |
| 26 | `docker compose` в `/home/kraken/mattermost/` — 3 сервиса (`postgres`, `mattermost`, сеть `mm-net`), БД `mm-postgres` | БД доступна как `docker exec mm-postgres psql -U mmuser -d mattermost` |
| 27 | 🔴🔴 **Приёмка от тестового аккаунта** — сквозной путь «работает», а заказчик получает `unauthorized` | тестировать **учёткой заказчика**; сверять `user_id` ленты с `principal` в `balda_user_bindings` (см. §«Ложная приёмка») |
| 28 | 🔴 **Дефект, найденный в БД, чинится только в БД** → возвращается при следующей записи | искать источник в коде; двойной префикс = helper, применённый поверх готового subject'а (см. §«Чинить в КОДЕ») |
| 29 | **Владельцем привязан чужой аккаунт** → `/start owner=` даёт «already registered», channel-токен для канала не создаётся, кандидат `Binding == nil` отсутствует | `UPDATE balda_user_bindings` (см. §«Дефект 8») — бэкап `state.db` + `-wal`, контейнер остановить |
| 30 | `sqlite3` по `balda_app_kv`: колонок `value`/`k` **нет** | схема: `namespace, key, value_json, updated_at, expires_at`; проверять `.schema` перед запросом |
| 31 | Пост **без** `@balda` отбрасывается (`dropping posted event: bot not mentioned`) | это НЕ дефект: `MentionsBot`; в канале писать `@balda <текст>`, в DM — без упоминания |
## Связанные заметки
@@ -1,10 +1,12 @@
# Фаза 3 — Балда на Kraken + Mattermost (деплой)
> Создан 2026-09-26. Обновлён 2026-09-26 (сессия реализации: `bad_seq`, MM-хендлер,
> привязка владельца, правки провайдера, **приёмка**).
> Статус: **✅ ФАЗА 3 ЗАКРЫТА. Сквозной путь `@balda` → ответ в UI работает и принят
> (образ `0.1.8`). Все шесть причин устранены и проверены. Push-уведомления —
> 🔴 невозможны на Team Edition, диагноз снят и задокументирован.**
> привязка владельца, правки провайдера; **🔴 статус приёмки ИСПРАВЛЕН**).
> Статус: **🔴 ФАЗА 3 НЕ ЗАКРЫТА. Бот отвечает `testuser`, но НЕ отвечает Alex.**
> Причина — аккаунт Alex не привязан как владелец (`unauthorized`); в `balda_user_bindings`
> владельцем сидит мой тестовый `testuser`. Alex: «Нихуя он мне не отвечает в ММ.
> Не готово!!» Образ `0.1.8`, все шесть технических причин устранены — осталась привязка
> аккаунта Alex (причина №7). Push-уведомления — 🔴 невозможны на Team Edition.
> Подробный план и фактические изменения — [[personal/plans/balda-mm-endtoend-plan]].
> Питфоллы и метод — [[personal/tech/balda-go-fx-pitfalls]].
> Часть проекта [[personal/projects/balda/balda-mm-delegation-plan]].
@@ -13,9 +15,56 @@
---
## ✅ ИТОГ (2026-09-26): бот работает, приёмка пройдена
## 🔴 СТАТУС: НЕ ГОТОВО — бот не отвечает аккаунту Alex
Лента канала `balda-test` (`7bjw4g7o1jdsbmjbqd649xxgaw`) — REST `GET /channels/{id}/posts`:
🔴 **Предыдущая запись «ИТОГ: бот работает, приёмка пройдена» была ЛОЖНОЙ.** Сквозной путь
проверялся от `testuser` (аккаунт, созданный для тестов), и именно `testuser` оказался
привязан владельцем. Alex пишет `@balda привет` — в логе `settlement_stage="unauthorized"`.
**Актуальная лента канала `balda-test` (`7bjw4g7o1jdsbmjbqd649xxgaw`):**
```
1790409344288 | cbwsorzwoigbtrdnteugnx4aqo | @balda привет ← Alex, БЕЗ ответа
1790409329367 | cbwsorzwoigbtrdnteugnx4aqo | Привет ← без @balda, отброшен намеренно
1790409148277 | edxrgupoxiyg5gzmr79tjypbme | работает ← ответ бота (testuser)
1790409145401 | c6udeze8dfrdxm5jbgq55bd58r | @balda reply with a single word
```
**Лог на пост Alex:**
```json
{"component":"balda.handlersfx.mattermost",
"inbound_id":"mattermost:dqagqce7pibiipt3qtqnkgfd8c",
"settlement_outcome":"terminal","settlement_stage":"unauthorized"}
```
**Корень (причина №7):** `balda_user_bindings` содержит одну строку —
`mattermost | c6udeze8dfrdxm5jbgq55bd58r` = **`testuser`**. Аккаунт Alex —
`cbwsorzwoigbtrdnteugnx4aqo` (`admin`, `system_admin`) — **в таблице отсутствует**.
`IsOwnerSubject` ищет строго по `(channel_type='mattermost', principal=<mm_user_id>)` →
для Alex пары нет → все проверки авторизации false.
📌 **Урок приёмки (durable): проверять ОТ ИМЕНИ ЗАКАЗЧИКА.** Сквозной путь,
подтверждённый чужим тестовым аккаунтом, — это НЕ приёмка. Если владелец — Alex,
тест обязан идти с его MM user_id. Подробный разбор замкнутого круга привязки —
[[personal/plans/balda-mm-endtoend-plan]] §«ПОЧЕМУ НЕ РАБОТАЕТ У ALEX».
**Остаток работы — одна строка в БД** (ожидает апрува Alex):
```sql
UPDATE balda_user_bindings
SET principal = 'cbwsorzwoigbtrdnteugnx4aqo',
display_name = 'mattermost:cbwsorzwoigbtrdnteugnx4aqo'
WHERE channel_type = 'mattermost'
AND principal = 'c6udeze8dfrdxm5jbgq55bd58r';
```
+ бэкап `state.db` с `-wal` до правки; + правка кода `handler.go:420` (двойной префикс,
см. §«Причина №7» в плане) и пересборка `0.1.9`, чтобы дефект не вернулся.
---
## 📋 ИСТОРИЯ: что реально работало (образ `0.1.8`, от `testuser`)
Лента канала `balda-test` на момент тестов от `testuser` — REST `GET /channels/{id}/posts`:
```
c6udeze8dfrdxm5jbgq55bd58r (testuser) | @balda reply with a single word
@@ -194,34 +243,42 @@ default-ветку — `/start` работает с MM без правок.
---
## ✅ Привязка владельца — ВЫПОЛНЕНА (2026-09-26)
## ✅ Привязка владельца — ВЫПОЛНЕНА, но ПРИВЯЗАН НЕ ТОТ АККАУНТ (2026-09-26)
Авторизация Balda — **subject-based**: владелец в `canonical` store привязан строкой
`<channel>:<principal>`. Для MM `channel_type='mattermost'`, `principal=<mm_user_id>`
**без** транспортного префикса.
**Итоговое состояние `balda_user_bindings`:**
**Итоговое состояние `balda_user_bindings` (актуальное):**
```
binding_id d2006cd1-3337-4159-883b-fcb90c6e9be2
user_id b6517e56-1110-4cb9-a301-302c585fbd0d ← администратор
channel_type mattermost
principal c6udeze8dfrdxm5jbgq55bd58r ← testuser, БЕЗ префикса
principal c6udeze8dfrdxm5jbgq55bd58r ← 🔴 testuser, НЕ аккаунт Alex
display_name mattermost:mattermost:c6udeze8… ← 🔴 мусор, двойной префикс остался
provenance verified-owner-onboarding
```
**Как это делалось (порядок, который сработал):**
🔴 **Это и есть причина, почему Alex не получает ответов.** Привязка сделана от
`testuser` — аккаунта, созданного мной для тестов. Аккаунт Alex
(`cbwsorzwoigbtrdnteugnx4aqo`) не привязан. Требуется `UPDATE` (см. §«СТАТУС» вверху).
⚠️ `display_name` с двойным префиксом — след того же дефекта кода (`handler.go:420`).
В прошлой сессии я поправил `principal` прямой правкой БД, но **не код**, поэтому мусор
в `display_name` и `created_by` channel-токенов остался, а дефект воспроизводится.
🔴 **Правило: чинить в коде, а не в данных.**
**Как привязка делалась (порядок, который сработал для первого аккаунта):**
1. `/start owner=<owner_auth_token>` **в DM** — команда `/start` работает только при
`Direct=true`, в канале отбивается. Скрипт `tools/mm-owner-bind.sh <user> <pass> <token>`:
логин → `POST /api/v4/channels/direct` с `[<my_id>, <bot_id>]` → `POST /api/v4/posts`
с текстом `/start owner=<token>`.
2. Получить токен:
2. Получить токен из `state.db`:
```bash
ssh kraken '/tmp/mm-owner-token.sh' # strings + sed по state.db под sudo -n
# или вручную:
ssh kraken 'sudo -n strings /srv/…-49e8f586…/docker/balda/state/state.db \
| grep -o "appowner_auth_token.\{0,45\}"' </dev/null
sqlite3 "$DB" "SELECT value_json FROM balda_app_kv WHERE key='owner_auth_token';"
# или: sudo -n strings …/state/state.db | grep -o "appowner_auth_token.\{0,45\}"
```
⚠️ Значение токена в доке **не хранится** — только способ получения.
3. 🔴 Первая попытка дала **«You are not allowed to use Balda in this channel.»** —
@@ -235,7 +292,7 @@ provenance verified-owner-onboarding
---
## Текущее состояние (2026-09-26, ФАЗА ЗАКРЫТА)
## Текущее состояние (2026-09-26, 🔴 ФАЗА НЕ ЗАКРЫТА)
| Компонент | Статус |
|---|---|
@@ -248,15 +305,17 @@ provenance verified-owner-onboarding
| Причина «bad_seq» (дельта 16) | ✅ **исправлена, проверена** |
| Причина «Processor==nil» (дельты 17-19) | ✅ **исправлена, доказана логом** |
| Причина «`/start` закрыт доступом» | ✅ **исправлена** |
| Причина «`principal` с префиксом» | ✅ **исправлена, авторизация проходит** |
| Причина «`principal` с префиксом» | ⚠️ **исправлена в БД, НО НЕ В КОДЕ** — дефект воспроизвёлся (см. причину №7) |
| Причина «провайдер `base_url` + CA» (дельта 22) | ✅ **исправлена, ПРОВЕРЕНА** |
| **Ответ бота в UI** | ✅ **ПОДТВЕРЖДЁН — бот ответил `работает` в `balda-test`** |
| **Ответ бота аккаунту Alex** | 🔴 **НЕ РАБОТАЕТ — `unauthorized`. Аккаунт Alex не привязан** |
| Ответ бота `testuser` | ✅ работает (это НЕ приёмка — чужой аккаунт) |
| Push-уведомления | 🔴 **невозможны на Team Edition** — диагноз снят, см. §Push ниже |
| MM-хендлер `handlersfx/mattermost_ingress.go` | ✅ написан, подключён в DI |
| Quality gates (build/vet/arch-lint) | ✅ все зелёные |
✅ **Приёмка пройдена: ответ бота виден в UI Mattermost** (проверено REST-чтением ленты,
`user_id` автора = бот). Фаза 3 закрыта.
🔴 **НЕ приёмка.** Требуется: привязать аккаунт Alex (`UPDATE balda_user_bindings`),
затем проверить `@balda привет` **от его аккаунта** и увидеть ответ в ленте канала.
Правку кода (`handler.go:420`) сделать в том же заходе — иначе двойной префикс вернётся.
---
+109 -6
View File
@@ -1,7 +1,7 @@
# Balda — Go/fx pitfalls и метод правок (зеркало скилла)
> Создано 2026-09-26. Обновлено 2026-09-26 (сессия реализации MM-хендлера, полный проход +
> **приёмка сквозного пути**, Фаза 3 закрыта).
> Создано 2026-09-26. Обновлено 2026-09-26 (сессия реализации MM-хендлера, полный проход;
> 🔴 **статус приёмки ИСПРАВЛЕН** — см. §«Ложная приёмка» и §«Чинить в коде, а не в данных»).
> Добавлены: **`bad_seq` — настоящая причина тишины WS** (§«WS-фрейм без seq»),
> **`optional:"true"` скрывает отсутствующую зависимость** (§«Дефект 5»),
> **`canonicalSubject` и неизвестный транспорт** (§«auth: subject-based»),
@@ -9,9 +9,11 @@
> **`base_url` в YAML мёртв — только `OPENAI_BASE_URL`** (§«Провайдер: …»),
> **`FROM scratch` без CA** (§«scratch и TLS»),
> **`principal` с префиксом в БД** (§«Дефект 7»),
> **`state.db` читать вместе с WAL** (§«Метод: чтение sqlite-состояния»).
> 🔴 **Новое в этой сессии (приёмка):** §«Приёмка агент-проверяемый» — как доказать ответ
> бота без GUI; §«Mattermost Team Edition: что недоступно» — push невозможен.
> **`state.db` читать вместе с WAL** (§«Метод: чтение sqlite-состояния»),
> **🔴 ложная приёмка от чужого аккаунта** (§«Ложная приёмка»),
> **🔴 двойной префикс жив в коде, а не только в БД** (§«Чинить в коде»),
> **🔴 замкнутый круг привязки при занятом binding** (§«Дефект 8»).
> 🔴 **Новое в этой сессии:** §«Mattermost Team Edition: что недоступно» — push невозможен.
> Уточнён §«авторизация — ОДИН механизм»: челлендж НЕ был причиной №1, им был `bad_seq`.
> Зеркало секции скилла `devops/docker-balda-service`, добавленной 2026-09-26.
> Источник: работа над Mattermost-адаптером Балды (Фаза 2) + деплой на Kraken (Фаза 3).
@@ -22,7 +24,104 @@
---
## ✅ Приёмка, проверяемая агентом (без GUI-браузера)
## 🔴🔴 Ложная приёмка: сквозной путь от ЧУЖОГО аккаунта — это не приёмка
**Самый дорогой урок сессии.** Я отчитался «фаза закрыта, приёмка пройдена», проверив
сквозной путь от `testuser` — аккаунта, созданного мной же для тестов. Alex написал
`@balda привет` — тишина, в логе `settlement_stage="unauthorized"`.
**Причина:** владельцем бота был привязан `testuser`. Аккаунта Alex в
`balda_user_bindings` **не было вообще**. Путь работал — но только для тестового аккаунта.
📌 **Правило приёмки:** проверять **от имени заказчика**, его учёткой. Если система
авторизует по `principal`/`user_id` — тест обязан идти с `user_id` заказчика.
Сквозной путь, подтверждённый подставным аккаунтом, доказывает **только** работоспособность
конвейера, но **не** работоспособность для пользователя.
🔴 **Красный флаг:** в тестовой ленте автором успешного запроса стоит НЕ тот `user_id`,
который будет в проде. Это уже повод не объявлять приёмку.
⚠️ Отличать от НЕ-дефекта: пост **без** упоминания бота отбрасывается намеренно —
`dropping posted event: bot not mentioned` (`MentionsBot`). В канале обращаться
`@balda <текст>`; в DM упоминание не нужно.
---
## 🔴 Чинить в КОДЕ, а не в данных (двойной префикс вернулся)
В прошлой сессии я нашёл, что `principal` в `balda_user_bindings` записан как
`mattermost:c6udeze…` (с префиксом), и починил это **прямым `UPDATE` в БД**. Дефект
**вернулся**: `display_name` остался `mattermost:mattermost:c6udeze…`, а `created_by`
новых channel-токенов снова получил двойной префикс.
**Настоящий источник — код**, `actors/command/start/handler.go:420`:
```go
case transportMattermost:
return auth.MattermostSubject(strings.TrimPrefix(trimmed, transportMattermost+":"))
```
`MattermostSubject()` (`auth/owner.go:307`) **сама** добавляет префикс вида `<channel>:<id>`,
а `p.Principal` приходит от MM-хендлера **уже с префиксом**
(`mattermost_ingress.go:147`: `Principal: auth.MattermostSubject(cmd.SenderID)`).
📌 **Правило: дефект, найденный в данных, искать и в коде.** Правка данных без правки
кода = дефект вернётся при следующей записи. Проверять **оба слоя** и фиксить источник.
⚠️ Тот же класс ошибки — двойное применение helper'а, который сам добавляет префикс.
Признак: строка вида `<x>:<x>:<id>` в БД, в `display_name`, в `created_by`, в логах.
Искать надо `MattermostSubject`/`TelegramSubject`-подобные билдеры, вызванные поверх
уже собранного subject'а.
⚠️ Остальные вызовы `MattermostSubject(cmd.SenderID)` в `mattermost_ingress.go`
(строки 189, 277, 292) **корректны** — они получают чистый `user_id`. Дефект ровно один,
в `userSubject` (`handler.go:420`).
---
## 🔴🔴 Дефект 8: замкнутый круг привязки, когда binding занят чужим аккаунтом
Ситуация: владельцем привязан **не тот** аккаунт, а нужно привязать правильный.
Все штатные пути оказываются закрыты.
| Штатный путь | Почему не работает |
|---|---|
| `/start owner=<owner_auth_token>` | `HasOwner()` = `len(OwnerSubjects()) != 0`. Владелец существует → код уходит в `handleExistingOwnerState` → `IsOwner(<нужный аккаунт>)` = false → **«Bot owner is already registered»** |
| `/start <channel_token>` для нужного канала | `CreateMissingOwnerBindTokens` (`auth/channel_token.go:215`) **пропускает** канал с `HasOwnerSubject(channel) == true`. Канал считается привязанным → **токен не создаётся вообще** |
| `registerOwner` → `attachCanonicalBinding` | Кандидат ищется как `Role == administrator` **И `Binding == nil`** (`canonical_users.go:110`). Единственный админ уже имеет binding → `no unbound active administrator user` |
📌 **Механика, которую надо помнить:**
- `HasOwner()` при `canonical != nil` — это `len(OwnerSubjects()) != 0`, а `OwnerSubjects()`
собирается из **binding'ов** (не из роли). Снять binding = обнулить `HasOwner()`.
- `AttachBinding` **не даёт перепривязать** (`count != 0` → `ErrBindingAlreadyAssigned`),
**detach-API отсутствует**.
- `purpose` channel-токена должен быть `owner_bind` — токен из другого назначения
`ConsumeOwnerBind` отвергнет.
🔴 **Единственный выход без правки кода** — `UPDATE` строки binding: меняем `principal`
(и `display_name`) на нужный аккаунт. Пользователь, роль и `HasOwner()` сохраняются,
связь «владелец ↔ аккаунт» переезжает. Обратимо обратным `UPDATE`.
```sql
UPDATE balda_user_bindings
SET principal = '<нужный mm_user_id>',
display_name = 'mattermost:<нужный mm_user_id>'
WHERE channel_type = 'mattermost'
AND principal = '<старый mm_user_id>';
```
⚠️ Перед `UPDATE` — бэкап `state.db` **вместе с `-wal`**; контейнер остановить
(sqlite не любит конкурентной записи). Значения проверять через
`sqlite3 … ".schema balda_app_kv"` — колонки `namespace, key, value_json, updated_at, expires_at`
(`value`/`k` таких колонок НЕТ).
📌 **Диагностика «почему именно мне бот не отвечает»:** сравнить `user_id` из ленты канала
с `principal` в `balda_user_bindings`. Несовпадение = `unauthorized`, и никакие логи
транспорта этого не покажут.
---
## ⚠️ Приёмка, проверяемая агентом (без GUI-браузера) — но ОТ НУЖНОГО АККАУНТА
Требование Alex: «приёмка = видно в UI, не "контейнер Up" и не "лог said accepted"».
Агент без браузера **не может** посмотреть UI — но может верифицировать ответ бота
@@ -44,6 +143,10 @@ edxrgupoxiyg5gzmr79tjypbme | работает ← это и есть док
⚠️ `post_user_id` в логах ингресса **не** подходит: там id отправителя запроса, не автора ответа.
⚠️ Не отчитываться «готово» по факту `delivery settled` в логах — доставляется ли пост, видно
только из ленты.
🔴🔴 **Обязательное условие: `user_id` отправителя в тесте должен быть аккаунтом ЗАКАЗЧИКА.**
Тест от подставного/тестового аккаунта доказывает работу конвейера, но не работу для
пользователя — см. §«Ложная приёмка» выше. Этот самый промах и дал ложный доклад «фаза
закрыта», после которого Alex написал «Нихуя он мне не отвечает в ММ. Не готово!!».
---