474 lines
26 KiB
Markdown
474 lines
26 KiB
Markdown
# Balda — Go/fx pitfalls и метод правок (зеркало скилла)
|
||
|
||
> Создано 2026-09-26. Обновлено 2026-09-26 (ночь, root-cause сессия) — добавлены:
|
||
> **WS-авторизация MM** (§«авторизация — ОДИН механизм»), **`Token:`-заголовок мёртв в MM 11.x**,
|
||
> **формат JSON дельт** (плоский массив + 3 аргумента у обёртки).
|
||
> Зеркало секции скилла `devops/docker-balda-service`, добавленной 2026-09-26.
|
||
> Источник: работа над Mattermost-адаптером Балды (Фаза 2) + деплой на Kraken (Фаза 3).
|
||
> Проектные доки: [[personal/projects/balda/balda-mattermost-connector]],
|
||
> [[personal/projects/balda/balda-kraken-mm-deploy]],
|
||
> [[personal/projects/balda/balda-mm-delegation-plan]].
|
||
|
||
---
|
||
|
||
## 🔴 `go build` = exit 0 ≠ рабочий транспорт
|
||
|
||
Компилятор не видит ошибок DI-графа. `go build ./...` проходит, а приложение падает
|
||
на старте с `fx.ValidateApp`. Проверять транспорт **только** реальным запуском валидации:
|
||
|
||
```go
|
||
prepared, err := loadBaldaCommandConfig(true) // нужен os.Chdir в tempdir с .config/balda/config.yaml
|
||
err = validateBaldaApplication(prepared) // ← вот это и есть проверка графа
|
||
```
|
||
|
||
Эталонный тест: `TestValidateCommandRunsValidationWithMattermostTransport`
|
||
в `cmd/balda/command_test.go`. На нём поймано 3 дефекта, которые `go build` пропустил.
|
||
|
||
---
|
||
|
||
## 🔴 Дефект 1: `fx.In` + внешний `fx.ParamTags`
|
||
|
||
**Симптом:**
|
||
```
|
||
Failed: invalid annotation function func(mattermostfx.IngressParams) (*mattermost.Ingress, error):
|
||
fx.In structs cannot be annotated with fx.ParamTags or fx.From
|
||
```
|
||
|
||
**Причина:** `fx.Annotate(NewIngress, fx.ParamTags(...))`, где `IngressParams` **уже** содержит `fx.In`.
|
||
Теги обязаны жить внутри структуры, снаружи аннотировать нельзя.
|
||
|
||
**Неправильно:**
|
||
```go
|
||
fx.Annotate(NewIngress,
|
||
fx.ParamTags(``, ``, ``, `name:"balda_mattermost_enabled"`,
|
||
`name:"balda_mattermost_bot_user_id"`,
|
||
`name:"balda_mattermost_bot_username"`, ``),
|
||
),
|
||
```
|
||
|
||
**Правильно:**
|
||
```go
|
||
// структура — теги на полях
|
||
type IngressParams struct {
|
||
fx.In
|
||
Processor mattermost.InboundProcessor `optional:"true"`
|
||
Client *mattermost.Client
|
||
Commands *commandcmd.Registry // обязательный, не optional
|
||
Enabled bool `name:"balda_mattermost_enabled"`
|
||
BotUserID string `name:"balda_mattermost_bot_user_id"`
|
||
BotUsername string `name:"balda_mattermost_bot_username"`
|
||
Logger zerolog.Logger
|
||
}
|
||
|
||
// предоставление — голое, без fx.Annotate
|
||
fx.Provide(NewIngress),
|
||
```
|
||
|
||
Эталон в апстриме: `zulip.ServerParams` + `zulip.NewServer` в `zulipfx/module.go` (строка 24) —
|
||
`zulip.NewServer` объявлен **без** `fx.Annotate`.
|
||
|
||
⚠️ Поле-порт (`*commandcmd.Registry`) делать **обязательным**. `optional:"true"` на нём — лишнее:
|
||
в графе он есть всегда, а optional маскирует ошибки конфигурации.
|
||
|
||
---
|
||
|
||
## 🔴 Дефект 2: нет провайдеров зарегистрированных строк
|
||
|
||
**Симптом:**
|
||
```
|
||
missing types:
|
||
- string[name="balda_mattermost_server_url"] (did you mean to Provide it?)
|
||
- string[name="balda_mattermost_token"]
|
||
- string[name="balda_mattermost_bot_user_id"]
|
||
```
|
||
|
||
**Причина:** модуль читает значения через `name:`-теги, а провайдеров этих имён в composition root нет.
|
||
У Zulip они есть (`balda_zulip_*`), для нового транспорта забыли.
|
||
|
||
**Правило: каждый `name:`-тег в модуле = обязательный провайдер в `app.go`.**
|
||
|
||
```go
|
||
fx.Provide(
|
||
fx.Annotate(
|
||
func() string { return strings.TrimSpace(cfg.Balda.Mattermost.ServerURL) },
|
||
fx.ResultTags(`name:"balda_mattermost_server_url"`),
|
||
),
|
||
),
|
||
```
|
||
|
||
`strings.TrimSpace` для отсутствующего ключа YAML даёт `""` — без паники, как у Zulip.
|
||
Образец: блок `balda_zulip_*` в `internal/apps/balda/app.go` (~строки 576-606).
|
||
|
||
---
|
||
|
||
## 🔴 Дефект 3: забытый импорт корневого пакета
|
||
|
||
`app.go:965: undefined: mattermost` при вызове `mattermost.ValidateConfig`, если импортирован
|
||
только `mattermostfx`. Транспорт требует **оба** импорта:
|
||
```go
|
||
"github.com/baldaworks/balda/internal/apps/balda/channel/mattermost"
|
||
"github.com/baldaworks/balda/internal/apps/balda/channel/mattermost/mattermostfx"
|
||
```
|
||
|
||
---
|
||
|
||
## 🔴🔴 Дефект 4: объявленная команда без actor-хендлера (роняет ВЕСЬ fx-граф)
|
||
|
||
Найден **только реальным запуском на Kraken** (Фаза 3). До этого Фаза 2 считалась закрытой:
|
||
`go build` = 0, `go vet` = 0, `golangci-lint` = 0 issues, `go-arch-lint` = OK, тесты зелёные.
|
||
**Ни один из этих гейтов его не видит.**
|
||
|
||
**Симптом** — crash-loop контейнера (`Restarting (1)`), в логах одна строка на тысячи
|
||
`"message":"provided"`:
|
||
|
||
```
|
||
Error: starting Balda app: could not build arguments for function
|
||
".../execution".init.func1: failed to build *execution.ActorHost: ...
|
||
failed to build *command.Router: received non-nil error from function
|
||
"reflect".makeFuncStub: advertised commands have no actor handler: goal
|
||
```
|
||
|
||
**Причина:** в `mattermostfx/module.go` в `commandcmd.Advertisement{Names: [...]}` была
|
||
объявлена команда `goal`, которой **не существует** как actor-хендлер. `goal` — это
|
||
**payload kind** (`goalkeepercmd.PayloadKindGoal`, `actorcmd.KindGoal`,
|
||
`actors/job_actor.go:100`), а снаружи цикл GoalKeeper называется `goalkeeper`.
|
||
|
||
**Механизм:** `actors/command/router.go:49 ValidateAdvertised()` вызывается из
|
||
`commandfx/module.go:306`. Он валидирует **весь** список `Names` одного advertisement и при
|
||
первом отсутствующем имени возвращает ошибку → fx не может построить `*command.Router` →
|
||
падает `*execution.ActorHost` → падает приложение. Одна опечатка в списке = полный отказ старта,
|
||
а не «недоступна одна команда».
|
||
|
||
**Правильно — сверять `Names` с реальными `Name()`:**
|
||
|
||
```bash
|
||
cd ~/Developer/balda
|
||
grep -rn 'func (h \*Handler) Name() string' internal/apps/balda/actors/command/
|
||
```
|
||
|
||
Эталонный фактический список (2026-09-26, `balda@de273d8f`):
|
||
|
||
```
|
||
locator · reset · usage · auto · cancel · close · start · user · topic · skill
|
||
goalkeeper · help · plugin
|
||
```
|
||
|
||
`mattermostfx/module.go` после фикса:
|
||
```go
|
||
Names: []string{
|
||
"locator", "reset", "usage", "auto", "cancel",
|
||
"goalkeeper", "topic", "close", "start",
|
||
"user", "skill",
|
||
},
|
||
```
|
||
`channel/mattermost/inbound.go` — тот же список в inbound allow-list, синхронно.
|
||
|
||
⚠️ `Zulip`-адаптер изначально был **прав** — там `"goalkeeper"` без `"goal"`. Регрессию внёс
|
||
MM-адаптер при копировании «на глаз».
|
||
⚠️ **`docs/commands.md` — НЕ источник истины** для имён команд. Там таблица доступности по
|
||
транспортам; реестр хендлеров — только `actors/command/*/handler.go`.
|
||
⚠️ Фикс — дельта-апплаером: `tools/deltas-goal-command-fix.json` (2 дельты, обе APPLIED).
|
||
|
||
📌 **Общий вывод (расширяет §«go build = exit 0 ≠ рабочий транспорт» выше):** недостаточно
|
||
проверять DI-граф тестом `validateBaldaApplication`. Тест должен прогоняться **с реальным
|
||
списком `Names` из модуля**. Но и это не поймало бы опечатку, если тест использует тот же
|
||
неверный advertisement — поэтому единственная надёжная проверка нового транспорта =
|
||
**живой запуск бинаря**. Для нового транспорта добавлять в чек-лист шаг:
|
||
`docker run` (или локальный `balda start`) с включённым транспортом **до** объявления готовности.
|
||
|
||
✅ **Фикс подтверждён живым запуском (2026-09-26).** После снятия `goal` контейнер `balda-mm`
|
||
на Kraken перестал падать и дошёл до рабочего состояния:
|
||
|
||
```
|
||
{"component":"balda.session_manager","balda_provider":"deepseek","message":"session manager ready"}
|
||
{"component":"balda.channel.mattermost.ingress","url":"ws://mattermost:8065/api/v4/websocket",
|
||
"message":"mattermost websocket connected"}
|
||
{"message":"Balda bot started. Press Ctrl+C to stop."}
|
||
```
|
||
|
||
📌 Порядок проверки, который сработал: правка дельтой → `go build ./...` → `go test
|
||
./internal/apps/balda/actors/command/...` → пересборка образа → `docker compose up -d` →
|
||
`docker logs | grep -E "started|websocket connected"`. **Не** объявлять транспорт готовым
|
||
по «exit 0» и «conтейнер Up» — только по этим двум строкам в логах.
|
||
|
||
---
|
||
|
||
## ⚠️ Env-override работает только по ключам, уже присутствующим в YAML
|
||
|
||
`appconfig/loader.go:367` — `SetEnvPrefix("BALDA")` + `SetEnvKeyReplacer(".", "_")` +
|
||
`AutomaticEnv()` **по leaf-ключам конфига**. Поэтому блок нового транспорта обязан быть
|
||
в `cmd/balda/balda.yaml` — иначе `BALDA_<X>_*` переменные **молча игнорируются**.
|
||
|
||
Порядок для нового транспорта:
|
||
1. блок в `cmd/balda/balda.yaml` (иначе env мёртв)
|
||
2. `MattermostConfig`-подобная структура + поле в `BaldaConfig`
|
||
3. провайдеры в `app.go`
|
||
4. `validate<X>Config` + вызов рядом с `validateSlackConfig`
|
||
5. включить в `validateBaldaChannelConfiguration` (`cmd/balda/command.go`) —
|
||
иначе деплой только на этом транспорте отбивается «at least one channel is required»
|
||
6. `.go-arch-lint.yml` — компоненты `<x>_transport`/`<x>_wiring` + права у
|
||
`composition_root` и `ingress_wiring` (см. §go-arch-lint выше)
|
||
7. тест на `validateBaldaApplication` с `<x>.enabled=true` (см. §«go build ≠ …» выше)
|
||
8. тест env-override в `cmd/balda/command_test.go` (по образцу
|
||
`TestLoadConfigDocument_AppliesZulipEnvOverrides`)
|
||
9. если добавлял маршрут в `BuiltinRoutes()` — обновить `TestBuiltinRoutes`
|
||
10. доки: `docs/reference/configuration.md` (ключи), `docs/commands.md` (колонка транспорта)
|
||
11. 🔴 **сверить `Advertisement.Names` с реальными `Name()`** в
|
||
`actors/command/*/handler.go` — опечатка роняет весь fx-граф (см. §«Дефект 4» выше)
|
||
12. 🔴 **живой запуск** `balda start` (или `docker run`) с включённым транспортом —
|
||
единственная проверка, которая ловит дефект 4
|
||
|
||
**Финальные quality gates (все обязательны, из `AGENTS.md`):**
|
||
```bash
|
||
go build ./... # 0
|
||
go vet ./... # 0
|
||
go tool golangci-lint run # 0 issues
|
||
go tool go-arch-lint check --project-path . # OK
|
||
go test -race ./...
|
||
```
|
||
⚠️ `go-arch-lint` **не входит** в `golangci-lint` — запускать отдельно, иначе ошибка
|
||
архитектуры всплывёт только на CI/ревью.
|
||
|
||
---
|
||
|
||
## 🔴 `go-arch-lint` — обязательный шаг для нового транспорта
|
||
|
||
⚠️ **Это не «опциональная придирка»: `AGENTS.md` прямо требует обновлять
|
||
`.go-arch-lint.yml` в том же изменении, что и архитектуру.** Без правки линтер падает
|
||
с двумя классами ошибок:
|
||
|
||
```
|
||
Component composition_root shouldn't depend on .../channel/mattermost
|
||
File /internal/apps/balda/channel/mattermost/adapter.go not attached to any component in archfile
|
||
```
|
||
|
||
**Что добавить в `.go-arch-lint.yml` (4 правки) — по образцу `zulip`:**
|
||
|
||
1. **Секция `components:`** — пары `in:`-компонентов:
|
||
```yaml
|
||
# Provider-owned Mattermost transport. It may depend only on
|
||
# transport-neutral contracts; application/runtime adapters live in wiring.
|
||
mattermost_transport:
|
||
in:
|
||
- internal/apps/balda/channel/mattermost
|
||
- internal/apps/balda/channel/mattermost/presentation
|
||
|
||
# Composition-root adapters for Mattermost delivery, prompt, and structured renderers.
|
||
mattermost_wiring:
|
||
in:
|
||
- internal/apps/balda/channel/mattermost/mattermostfx
|
||
```
|
||
⚠️ Root-пакет и `presentation/` идут в **один** компонент `_transport`, а `_fx`-пакет —
|
||
отдельно в `_wiring`. Именно так у `zulip`/`telegram`/`slackagent`.
|
||
|
||
2. **Правила `mayDependOn`:**
|
||
- `mattermost_transport` → `foundation, contracts, neutral_registry, mattermost_transport`
|
||
- `mattermost_wiring` → `foundation, contracts, neutral_registry, application, delivery,
|
||
storage, mattermost_transport, mattermost_wiring`
|
||
|
||
3. **`composition_root`** → добавить `mattermost_transport`, `mattermost_wiring`
|
||
(иначе «composition_root shouldn't depend on …» — это ожидаемый отказ, там
|
||
перечислены все транспорты).
|
||
|
||
4. **`ingress_wiring`** → добавить `mattermost_transport` (соседствует с `telegram_transport`,
|
||
`zulip_transport`).
|
||
|
||
Проверка: `go tool go-arch-lint check --project-path .` → должно быть `OK - No warnings found`.
|
||
|
||
---
|
||
|
||
## 🔴 `golangci-lint` на новом транспорте: мёртвый typing-throttle
|
||
|
||
Первый прогон дал 4 замечания — все в новом коде. Важный урок: **не гейтить линтер
|
||
nolint-комментарием, а удалять мёртвый код.**
|
||
|
||
| Замечание | Что было | Фикс |
|
||
|---|---|---|
|
||
| `inamedparam` ×2 | `Start(context.Context) error` в interface-assertion | `Start(ctx context.Context) error` |
|
||
| `unused` | `shouldSendTyping()` — недостижим, т.к. `SendTyping` всегда no-op | удалён |
|
||
| `unused` | `websocketReceiveTimeout` — константа без использования | удалена |
|
||
| `unlambda` | `func(client, logger) *Adapter { return NewAdapter(...) }` | заменено на `mattermost.NewAdapter` |
|
||
|
||
**Корневая причина `unused`:** механизм typing-throttle (`shouldSendTyping`, поля
|
||
`typingThrottleInterval`/`typingLastSentAt`, `typingMu sync.Mutex`,
|
||
`SetTypingThrottleInterval`) был скопирован из Zulip по аналогии. Но у Mattermost
|
||
**ботов нет typing API** (`SendTyping` осознанно возвращает `nil`), поэтому весь механизм
|
||
недостижим. Удалён целиком — не заглушён.
|
||
|
||
**Правило: при копировании транспорта с Zulip проверять каждую фичу на применимость.**
|
||
Zulip-специфичное, что НЕ переносится в транспорт без поддержки:
|
||
- typing-throttle (нужен интерактивный typing API)
|
||
- логика `sendWithPlainFallback` — переносится, но только на content-rejection
|
||
|
||
⚠️ **Каскад после удаления полей:** удаление `typingThrottleInterval` потянуло `SetTypingThrottleInterval`
|
||
→ `sync` (импорт) → `zerolog` (импорт в `module.go`). Удалять надо **всей группой**,
|
||
иначе серия `undefined`/`imported and not used`.
|
||
|
||
---
|
||
|
||
## ✅ Тест-регрессия: `BuiltinRoutes` и длина списка
|
||
|
||
Добавление маршрута в `BuiltinRoutes()` (`deliveryfmt/routes.go`) **ломает** `TestBuiltinRoutes`
|
||
в `deliveryfmt/registry_test.go`, если не обновить ожидание:
|
||
|
||
```
|
||
len(BuiltinRoutes()) = 5, want 4
|
||
```
|
||
|
||
Фикс — добавить строку в map `want`:
|
||
```go
|
||
{transport: TransportMattermost, deliveryFormat: DeliveryFormatNone}: NamePlainText,
|
||
```
|
||
📌 Нашлось только сравнением «с правками / без правок» через `git stash` — в списке
|
||
предсуществующих падений апстрима этого теста **не было**, значит это наша регрессия.
|
||
|
||
---
|
||
|
||
## ✅ 13 пакетов падают в апстриме изначально
|
||
|
||
Не пытаться «чинить» как часть своей задачи — это дефект апстрима. Проверять `git stash`
|
||
перед выводами о регрессии.
|
||
|
||
Падающие в `baldaworks/balda@de273d8f`:
|
||
- `TestInitCommand_NonInteractiveAutoSelectsRootAndGeneratesDetectedAgents` (cmd/balda)
|
||
- `TestMigrateSessionMemoryLayout_*` ×4 (state)
|
||
- `TestWorkspaceMCPImport/Export_UsesPersistedSessionMetadata` (session)
|
||
- `TestPluginResolverResolvesContainedArgvFilteredEnvAndLaunchSecrets` (mcpruntime)
|
||
- `TestResolveWorkspaceBaseBranch_ConfigPreferredWhenValidVal`, `TestCanonicalSessionMemory*` (apps/balda)
|
||
|
||
**Метод проверки регрессии:**
|
||
```bash
|
||
git stash push -q -m baseline --include-untracked
|
||
go test ./<пакет>/ -count=1 # смотрим падения БЕЗ правок
|
||
git stash pop -q # правки возвращаются
|
||
```
|
||
⚠️ `git stash` может задеть чужие стэши — в форке лежат `whale-20260623` и
|
||
`feat/zulip-events-polling`. Проверять `git stash list` до и после.
|
||
|
||
---
|
||
|
||
## Метод правок: дельта-апплаер вместо sed
|
||
|
||
Правило Alex: **никаких in-place скриптов и sed**. Инструмент в `~/Developer/balda/tools/`:
|
||
|
||
| Файл | Роль |
|
||
|---|---|
|
||
| `apply-project-deltas.py` | движок (JSON-дельты + состояние) |
|
||
| `run-project-deltas.sh` | обёртка `check` / `apply` / `list` |
|
||
| `deltas-<topic>.json` | группы правок |
|
||
| `.deltas-state.json` | sha256 применённых якорей |
|
||
|
||
```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
|
||
```
|
||
|
||
**Формат дельты:**
|
||
```json
|
||
{ "file": "путь/от/корня", "note": "что делаем",
|
||
"search": "уникальный фрагмент текущего текста",
|
||
"replace": "чем заменить" }
|
||
```
|
||
|
||
**Три исхода:** `replace` уже есть → `skipped` (идемпотентно); `search` ровно 1 раз → `applied`;
|
||
ни того ни другого → `error: ЯКОРЬ НЕ НАЙДЕН`.
|
||
|
||
📌 `check` **обязателен перед `apply`** — на нём поймано, что дельта валидации целилась
|
||
в несуществующий `validate()` в `config.go` (валидация живёт в `app.go`), и переписана до записи.
|
||
|
||
⚠️ Дельты с длинным `replace` писать в JSON через `write_file`, **не** heredoc — кавычки и `\n`
|
||
ломаются молча. Это тот же питфолл, что у SSH-heredoc с вложенными кавычками.
|
||
|
||
🔴 **JSON дельт — ПЛОСКИЙ МАССИВ, не объект.** Формат-обёртка
|
||
`{"deltas": [...]}` (или любой объект с ключами) роняет апплаер:
|
||
|
||
```
|
||
TypeError: string indices must be integers
|
||
File "apply-project-deltas.py", line 91, in main
|
||
path = root / d["file"]
|
||
```
|
||
|
||
Причина: `main()` итерирует `for i, d in enumerate(deltas)` — если `deltas` объект, `d`
|
||
становится строкой-ключом. Проверено 2026-09-26 на `deltas-mattermost-14.json`.
|
||
**Правильно:** файл начинается с `[`, а не с `{`.
|
||
|
||
⚠️ Обёртка `run-project-deltas.sh` требует **ТРИ** аргумента, а не два:
|
||
`run-project-deltas.sh <check|apply|list> <deltas.json> <state.json>`.
|
||
Без третьего — `line 9: 3: нужен файл состояния`.
|
||
И `check` не поддерживает `--list`: `list` — отдельный глагол (`$1 = list`).
|
||
|
||
---
|
||
|
||
## 🔴 Mattermost WebSocket: авторизация — ОДИН механизм, не два
|
||
|
||
Найдено живым запуском (Фаза 3, root-cause). Симптом: в логе
|
||
`mattermost websocket connected`, но **ни одного** события `posted` в инбаунде, даже
|
||
на `logger.level: debug`. При этом пробник (pure Go stdlib) ловил `posted` на том же сокете.
|
||
|
||
**Две причины, наложившиеся друг на друга:**
|
||
|
||
### 1. `Origin` в handshake → 403 CORS
|
||
|
||
Если клиент шлёт `Origin: http://mattermost:8065`, а `SiteURL=https://mm.qentra.top` —
|
||
MM отвечает:
|
||
|
||
```
|
||
HTTP/1.1 403 Forbidden
|
||
{"id":"api.web_socket.connect.upgrade.app_error","message":"URL Blocked because of CORS. Url: …"}
|
||
```
|
||
|
||
📌 **`Origin` не слать вообще.** Балда его не шлёт — с ней этой проблемы нет.
|
||
Это ловушка для пробников и внешних клиентов.
|
||
|
||
### 2. `authentication_challenge` поверх `Authorization: Bearer` → подписка умирает
|
||
|
||
`channel/mattermost/ingress.go`, `streamOnce`:
|
||
|
||
```go
|
||
conn, _, err := dialer.DialContext(ctx, endpoint, header) // header: Authorization: Bearer ***
|
||
// …
|
||
s.logger.Info().Msg("mattermost websocket connected") // ← лог УЖЕ «здоровый»
|
||
conn.WriteJSON(map[string]any{ // ← 🔴 а это ломает подписку
|
||
"seq": 1, "action": "authentication_challenge",
|
||
"data": map[string]string{"token": token},
|
||
})
|
||
```
|
||
|
||
**Эмпирика (пробник, `-authheader`):**
|
||
- с челленджем: `hello` → `status_change` → **тишина 40 с**, хотя пост в канал создаётся;
|
||
- без челленджа: `hello` → `posted` штатно.
|
||
|
||
То есть избыточный челлендж заставляет MM переинициализировать сессию сокета, и `posted`
|
||
на него больше не пушится. **Диагностически коварно:** `websocket connected` логируется
|
||
**до** челленджа, поэтому лог выглядит здоровым.
|
||
|
||
**Правило:** сокет авторизуется **либо** заголовком на dial, **либо** `authentication_challenge`.
|
||
Оба сразу — тишина. Если заголовок есть, челлендж слать **нельзя**.
|
||
|
||
**Фикс** — дельта `tools/deltas-mattermost-14.json` (2 дельты: убрать `WriteJSON`-блок
|
||
и unused-константу `websocketActionAuthChallenge`). Образ `balda-mm:0.1.2`.
|
||
|
||
📌 **Общий урок для транспортов:** лог «connection established» ничего не говорит о том,
|
||
что подписка жива. Проверять **фактом получения события**, а не фактом коннекта.
|
||
Для MM — пробник из `~/Developer/balda-wsprobe/` (pure stdlib, RFC6455, собирается нативно
|
||
на Mac, на хост кладётся статический бинарь).
|
||
|
||
### 🔴 MM 11.x: заголовок `Token:` для API мёртв
|
||
|
||
`POST /users/login` отдаёт сессию в **заголовке ответа** `Token:`. Но использовать её нужно
|
||
**только** как `Authorization: Bearer *** — заголовок `Token: <session>` в запросах даёт
|
||
`401 api.context.session_expired.app_error` при живой сессии (в БД `expiresat` = +180 дней).
|
||
Старый стиль MM 5.x больше не принимается. Проверено 2026-09-26.
|
||
|
||
📌 Отсюда же: если скрипт падает с `session_expired` — сначала проверить, каким **именем
|
||
заголовка** шлётся токен, а не искать проблему в пароле/лимите сессий.
|
||
|
||
---
|
||
|
||
## Связанные заметки
|
||
|
||
- [[personal/projects/balda/balda-mattermost-connector]] — Фаза 2, где всё это найдено
|
||
- [[personal/projects/balda/balda-mm-delegation-plan]] — общий план
|
||
- [[personal/tech/hermes-secret-redaction-bypass]] — маскировка секретов в tool-выводе
|