Files
obsidian-vault/personal/plans/runtime-pr5-toolset-dedup-review-fixes.md
T

157 lines
11 KiB
Markdown
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
> Создан 2026-09-28. Статус: **✅ РЕАЛИЗОВАНО, ЗАПУШЕНО. Ре-ревью ожидается.**
> Голова PR: **`d4ab923`** (4 файла, +411/−1), ветка `fix/hosted-toolsets-duplicate-endpoint`.
> **PR normahq/runtime#5** — OPEN. Ответ ревьюеру опубликован:
> https://github.com/normahq/runtime/pull/5#issuecomment-5867224210
> Репо: `~/Developer/runtime-local`. Связано: [[personal/plans/runtime-pr4-openai-toolcalls-fixes]].
---
## 0. ИТОГ — что сделано
Коммит **`d4ab923`** `fix(agentfactory): dedup MCP toolsets by explicit key`.
**Гейты локально — все зелёные:**
```
gofmt -l agentfactory/ agentconfig/ → чисто
go build ./... → OK
go vet ./... → OK
go test ./... → все 10 пакетов ok
go test -race ./... → все ok
go tool golangci-lint run ./... → 0 issues
```
| # | Дефект ревью | Фикс |
|---|---|---|
| 1 | **P1** ключ стирал query и игнорировал `Headers` → слияние разных тенантов/авторизаций | `mcpEndpointKey` + `mcpURLIsScoped` **удалены**. Инференс из URL убран. Схлопывание только по явному непустому `DedupKey`; пусто → **никогда** не схлопывается |
| 2 | **P2-1** `selectScopedOnly` копировал алгоритм прода | **удалён**. Тесты пишут endpoint каждого конфига, реально дошедшего до `mcpTransportFactory` (инъектируемая переменная), и ассертят на нём |
| 3 | **P2-2** `ProcessRequest(ctx any, req any)` — вакуумный | Точная продакшн-сигнатура `(agent.Context, *model.LLMRequest)` + позитивный контроль `TestRequestProcessorInterfaceIsSatisfiable` |
**Новые поля `agentconfig.MCPServerConfig`** (аддитивно, по умолчанию выключено):
- `DedupKey` — явный ключ эквивалентности (opaque, задаёт вызывающий);
- `DedupPreferred` — кто выживает в группе, когда записи не взаимозаменяемы.
Приоритет победителя: помеченный `DedupPreferred` > лексикографически меньший id
(детерминированно, не зависит от порядка map).
**Balda — composition layer (как просил ревьюер):**
- `sessionmemorymcp.BundledDedupKey = "balda-bundled-mcp"` — общий ключ;
- `internal/apps/balda/internalmcp/manager.go:213` — bundled-запись получает `DedupKey`;
- `internal/apps/balda/sessionapp/module.go:130` — scoped-binding получает `DedupKey` + `DedupPreferred: true`.
**Тесты (8 в `agentfactory`):** `TestHostedToolsetsCollapsesScopedEndpointDuplicate`,
`TestDistinctQueryParamsNeverCollapse`, `TestDistinctHeadersNeverCollapse`,
`TestSameDedupKeyCollapsesThreeEntries`, `TestDedupPreferredDecidesPlaceholderWinner`,
`TestDedupWithoutPreferenceIsLexicographicallySmallest`, `TestEmptyDedupKeyNeverCollapses`,
`TestHostedToolsetsKeepsDistinctEndpoints`, `TestMcpToolsetRequestProcessorSignature`,
`TestRequestProcessorInterfaceIsSatisfiable`.
### 🔴 Негативные контроли (доказано, что тесты не вакуумные)
Проверено скриптом `/tmp/negctl.sh`:
1. Вернул инференс из URL (стрип query) → `TestDistinctQueryParamsNeverCollapse` **FAIL**:
`expected 2 toolsets for distinct tenants, got 1`. Лог: `skipping duplicate toolset "tenant-b": dedup key "http://h:1/mcp" already served by "tenant-a"`.
2. Вернул `ctx any, req any` → **build failed**:
`*stubRequestProcessor does not implement requestProcessor (wrong type for method ProcessRequest)`.
3. Файлы восстановлены **байт-в-байт** (`diff` чистый), ре-верификация зелёная.
---
## 1. 🔴 КЛЮЧЕВОЕ ОТКРЫТИЕ: scoped-запись ОБЯЗАНА выживать
Изначально планировался лексикографический победитель (`balda` < `balda-session-memory-*`),
т.е. **plain** запись. При проверке кода брокера выяснилось — это сломало бы session-memory:
`internal/apps/balda/sessionmemorymcp/broker.go`:
- `contextQueryParameter = "balda_context"` — токен читается **из query** (`:124`);
- `Wrap` (`:118-150`): без токена запрос идёт дальше без инъекции `HeaderSessionLocator`/
`HeaderSessionID`/`HeaderSessionBinding`;
- комментарий в коде: *«session-memory calls still fail closed in HeaderSessionResolver»*.
→ Токен в URL **функциональный**, а не косметический. Поэтому:
1. нужен был `DedupPreferred` (порядок id не подходит);
2. заметка в тесте исправлена: `?ctx=` → реальный `?balda_context=<token>`.
⚠️ **Вывод на будущее:** старая эвристика `mcpURLIsScoped` («есть query → scoped») давала
**правильного** победителя случайно. Замена на «лексикографически меньший» без
`DedupPreferred` была бы регрессией. Проверка кода, а не симметрия с ревью, её поймала.
---
## 2. Что говорит ревью (дословно, 3 пункта)
Ревьюер **metalagman**, `CHANGES_REQUESTED`, 2026-09-28T09:04:10Z (опубликовано в PR).
| # | Severity | Суть |
|---|---|---|
| 1 | **P1 — blocking** | `mcpEndpointKey` выбрасывает все query-параметры, группировка игнорирует `Headers`. `/mcp?tenant=a` vs `?tenant=b`, или один URL с разными `Authorization`, схлопываются в один произвольный конфиг → доступ к чужому tool-set / пересечение границы доступа. Просьба: *«keep generic endpoint identity exact»* + *«resolve the Balda-specific duplicate in the Balda composition layer, or introduce explicit alias/replacement metadata»* |
| 2 | **P2** | `selectScopedOnly` повторяет алгоритм продакшена и вычисляет победителя сам — не проверяет фактически удержанный toolset |
| 3 | **P2** | `ProcessRequest(ctx any, req any)` — Go требует точных типов, реальный метод интерфейс не удовлетворит; ассерт не может сработать |
**Validation ревьюера:** `go test -race ./...`, `go vet ./...`, `golangci-lint run ./...` — проходили.
`no check runs/statuses` — тот же fork-блокер, что в PR #4.
---
## 3. ⛔ CI: внешний блокер (не наш дефект)
`gh pr checks 5` → `no checks reported on the 'fix/hosted-toolsets-duplicate-endpoint' branch`.
Причина та же, что в PR #4 (§0в в [[personal/plans/runtime-pr4-openai-toolcalls-fixes]]):
**fork-PR** (`mallexxx/runtime` → `normahq/runtime`), workflows ждут одобрения мейнтейнера.
Починить не можем. Локальные гейты эквивалентны CI (`test.yml` + `lint.yml`).
---
## 4. Питфоллы, найденные в работе
1. 🔴 **`write_file` с относительным путём пишет НЕ в vault.** Первый `write_file` с путём
`personal/plans/runtime-pr5-...md` создал файл в `/Users/admin/Developer/balda/personal/...`.
Для vault — **только `mcp_obsidian_write_note`** (vault root `/Users/admin/obsidian`).
2. ⚠️ **`rm -rf` в терминале требует подтверждения** — таймаут апрува заблокировал команду.
Лишнее дерево `/Users/admin/Developer/balda/personal/` **не удалено** (см. §6).
3. ⚠️ **LSP-диагностика после `patch` бывает устаревшей** — показывала `undefined: mcpEndpointKey`
после того, как `go build` уже был чист. Верить `go build`/`go test`, не LSP-снапшоту.
4. ⚠️ Правки — **только `patch`**, не sed/in-place (правило Alex). Ни одного sed не использовано.
5. ⚠️ Untracked `tools/` в репо — **не мой** файл, не коммитил.
---
## 5. Осталось сделать
1. ⏳ **Ре-ревью metalagman** — ждать.
2. ⛔ **Синхронизация копии рантайма в Balda** (`~/Developer/balda/runtime-local`, HEAD `05342c5a`,
`grep -c DedupKey agentconfig/config.go` → **0**). Правки Balda-слоя (§0) **сейчас не компилируются**
против этой копии — ошибки `unknown field DedupKey in struct literal of type agentconfig.MCPServerConfig`.
Это **отдельная задача**, как и в PR #4 (решать после мержа, чтобы копия не разъехалась снова).
🔴 Пока не синхронизировано — боевой стенд Балды фикс **не получил**.
3. ⚠️ `/tmp/negctl.sh`, `/tmp/control1.py`, `/tmp/control2.py`, `/tmp/af.bak`, `/tmp/dt.bak`,
`/tmp/commitmsg.txt`, `/tmp/reply.md`, `/tmp/pr5_af.go`, `/tmp/pr5_test.go` — временные.
4. ⚠️ Проверить, не утекла ли ветка PR #4 (`feat/openai-tool-calls-v2`) — она тоже открыта.
---
## 6. Требует решения Alex
Удалить лишнее дерево, созданное ошибочным `write_file` (содержит только пустые
`plans/` + `tech/`, ничего ценного):
```
/Users/admin/Developer/balda/personal/
```
Удаление требует явного подтверждения — не трогал.
---
## 7. Контекст PR (исходная проблема)
`hostedagent`/`agentfactory` — ADK разворачивает каждый toolset в один плоский список
инструментов, а упаковка **не идемпотентна**: два сервера с одинаковым именем инструмента
аварийно завершают ход с `duplicate tool: "balda.control.shutdown"`.
Balda регистрирует bundled MCP-сервер дважды: как `"balda"` и как session-scoped binding того же
listener'а с токеном в query. Оба отдают один список инструментов → каждый tool-using ход падал.
Исходный фикс (`b01bdc6`) схлопывал по host/path — что и открыло P1.
## Связанные заметки
- [[personal/plans/runtime-pr4-openai-toolcalls-fixes]] — PR #4, fork-CI-блокер, вопрос синхронизации копии
- [[personal/tech/adk-hostedagent-openai-mcp]] — архитектура MCP/toolset, `runtime.mcp_servers`
- [[personal/tech/balda-go-fx-pitfalls]] — метод дельт, питфоллы