diff --git a/personal/plans/balda-mm-endtoend-plan.md b/personal/plans/balda-mm-endtoend-plan.md index e22c40fb..0ed8d515 100644 --- a/personal/plans/balda-mm-endtoend-plan.md +++ b/personal/plans/balda-mm-endtoend-plan.md @@ -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=)`. +Пары для 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=` | `HasOwner()` = `len(OwnerSubjects()) != 0`. Владелец (`testuser`) есть → идёт в `handleExistingOwnerState` → `IsOwner(Alex)` = false → **«Bot owner is already registered»** | +| `/start ` для 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 — без упоминания | ## Связанные заметки diff --git a/personal/projects/balda/balda-kraken-mm-deploy.md b/personal/projects/balda/balda-kraken-mm-deploy.md index 45f7eb5c..1b6a833a 100644 --- a/personal/projects/balda/balda-kraken-mm-deploy.md +++ b/personal/projects/balda/balda-kraken-mm-deploy.md @@ -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=)` → +для 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 привязан строкой `:`. Для MM `channel_type='mattermost'`, `principal=` **без** транспортного префикса. -**Итоговое состояние `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=` **в DM** — команда `/start` работает только при `Direct=true`, в канале отбивается. Скрипт `tools/mm-owner-bind.sh `: логин → `POST /api/v4/channels/direct` с `[, ]` → `POST /api/v4/posts` с текстом `/start owner=`. -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\}"' Создано 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`) **сама** добавляет префикс вида `:`, +а `p.Principal` приходит от MM-хендлера **уже с префиксом** +(`mattermost_ingress.go:147`: `Principal: auth.MattermostSubject(cmd.SenderID)`). + +📌 **Правило: дефект, найденный в данных, искать и в коде.** Правка данных без правки +кода = дефект вернётся при следующей записи. Проверять **оба слоя** и фиксить источник. + +⚠️ Тот же класс ошибки — двойное применение helper'а, который сам добавляет префикс. +Признак: строка вида `::` в БД, в `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=` | `HasOwner()` = `len(OwnerSubjects()) != 0`. Владелец существует → код уходит в `handleExistingOwnerState` → `IsOwner(<нужный аккаунт>)` = false → **«Bot owner is already registered»** | +| `/start ` для нужного канала | `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 написал «Нихуя он мне не отвечает в ММ. Не готово!!». ---