Хвосты по ревью PR #172: тот же jsonb double-encoding в jsonbObject (роли агента) + централизация и тесты #173
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Хвосты по ревью PR #172 (
fix(mcp): tool allowlist stored/read as jsonb string, not array). Сам PR корректен и смержен — здесь то, что надо доделать отдельно. Пункты 1–2 — из архитектурной части ревью (взят Вариант A в обоих), остальное — замечания из того же ревью.1. Починить тот же баг double-encoding в
jsonbObject(роли агента) — приоритетТот же класс ошибки, что чинил PR #172 для
jsonbArray, остался нетронутым в соседнем хелпере.apps/server/src/database/repos/ai-agent-roles/ai-agent-roles.repo.ts:177—jsonbObjectпишетsql\${JSON.stringify(value)}::jsonb`, байт-в-байт баговая форма (диалектkysely-postgres-js`/postgres.js пере-сериализует параметр → в колонку ложится jsonb string scalar, а не объект).ai_agent_roles.model_configпочти наверняка уже хранит double-encoded строки. Отказ тихий: read-путь делаетtypeof cfg !== 'object'→ строковый override молча игнорируется и роль откатывается к дефолтной модели (аtoViewотдаёт в UI строку вместо объекта).::jsonb→::text::jsonb;parseModelConfig+normalizeRow(чинит уже испорченные строки на чтении, без миграции);insertпроверитьjsonb_typeof(model_config) = 'object'и что чтение отдаёт объект.2. Централизовать обход jsonb double-encoding (Вариант A)
Один и тот же трюк уже переоткрыт трижды:
workspace.repo.ts(::text-касты +jsonb_typeofself-heal),ai-mcp-server.repo.ts(PR #172) иai-agent-roles.repo.ts(ещё на баговой форме, см. п.1). Дублирование уже разъехалось.jsonbBind()(write) вapps/server/src/database/utils.ts, заменить per-repojsonbArray/jsonbObjectна один реализованный-один-раз хелпер с одним комментарием-объяснением квирка.parseJsonbArray/parseJsonbObject), куда переедут юнит-тесты.jsonb_build_object-пути вworkspace.repo.tsструктурно другие — остаются отдельными. Глобальный кодек на уровне Kysely/диалекта сознательно НЕ берём (непропорциональный риск ради ~3 колонок, задевает тяжёлый ProseMirror-контент страниц).3. Интеграционный тест на write-каст
::text::jsonbдля allowlist — надоapps/server/test/integration/ai-mcp-server-repo.int-spec.ts: послеrepo.insert({ toolAllowlist: ['a','b'] })проверить сырым запросомjsonb_typeof(tool_allowlist) = 'array'и чтоfindByIdвернул настоящийstring[]; опционально засеять заведомо double-encoded строку и проверить, чтоlistEnabled/findByIdеё чинят.parseToolAllowlistмаскирует регресс: откат каста обратно на::jsonbоставит все юнит-тесты зелёными, а колонку снова молча испортит. Харнесс уже есть (test:int/apps/server/test/jest-integration.json, рядом — другие*.int-spec.ts).4. Обновить устаревший комментарий типа сущности — надо
apps/server/src/database/types/ai-mcp-servers.types.ts:22-24— строка «reads come back as a string[] from the postgres driver» теперь вводит в заблуждение (драйвер для legacy-строк отдаёт JSON-строку;string[]гарантирует ужеnormalizeRow/parseToolAllowlist). Переписать, например: «Stored as jsonb. The pg driver may return a JSON string for legacy double-encoded rows;AiMcpServerReponormalizes every read tostring[] | nullviaparseToolAllowlist.»5. Логировать fail-open при битом allowlist — надо
apps/server/src/database/repos/ai-chat/ai-mcp-server.repo.ts(parseToolAllowlist): в «corrupt»-ветках (строка не распарсилась / распарсилась в не-массив / не-строковые элементы) писать одну строкуwarnс id сервера (без содержимого), прежде чем вернутьnull. Сейчас битый allowlist молча означает «без ограничений» → агенту уходят все инструменты сервера, без следа в логах.6. Упростить
parseToolAllowlist— по желаниюИсточник: код-ревью PR #172.
Ghost referenced this issue2026-06-25 12:00:53 +03:00