Мой продукт — программа для ретуши и плагин для Photoshop с локальной нейросетью. Чтобы выпуская программу и обеспечить её безопасность, я выстроила десять слоев безопасности: защищённый канал между плагином и системной службой, регистрация устройства, подпись артефактов с меткой времени, аттестация сборки, канал обновлений, шифрование модели, водяной знак. Части написаны на C++ и Go, живут в разных процессах и ставятся разными установщиками.
Каждая такая граница — это не модуль, а договорённость двух сторон: одно имя службы, одна версия протокола, один набор маршрутов, одна строка подтверждения. Сейчас 54 связи на этих границах держат 188 сравнений значений (468 мест в коде) и 105 сравнений множеств. Компилятор не видит ни одного из них: стороны написаны на разных языках.
Я назвала такие места швами — точками, где разные компоненты должны точно совпадать:
маршрут, который вызывает клиент и регистрирует сервер;
заголовок с версией протокола;
поля DTO в Go и TypeScript;
переменные окружения, которые читает сервис и которые указаны в
.env.example;правило округления цены, реализованное на двух языках.
Типы, тесты и линтеры хорошо работают внутри одного языка, но такие договорённости обычно не проверяют. Они держатся на памяти разработчиков.
Инструмент называется Crossweft (уток, weft, — поперечная нить в ткани: она проходит через все продольные и держит полотно вместе).
С чего все началось
Разработку я начала в январе этого года. За это время я потратила на подписки больше $9 000 — по самым скромным прикидкам это больше миллиарда токенов. И всё это время я не понимала, почему у меня не сходится безопасность. Сама программа работала прекрасно, а соединить слои защиты не получалось. Я даже сделала отдельное приложение, которое следило за «сердцебиением» слоёв, чтобы свести их вместе, — но и это было безуспешно.
Под капотом у программы четыре части: клиент, системная служба, бэкенд и установщики. И вот три случая, после которых у меня наконец сложилось понимание, как свести агентскую разработку воедино…
Один идентификатор — три копии. Имя службы определяли в трёх почти одинаковых функциях. На чистой машине служба ещё не была установлена, поиск заканчивался ошибкой, и пустая машина выглядела как старая несовместимая установка — установщик останавливался с кодом 76. Исправила одну копию — две другие остались сломанными.
Две половины одной зависимости. Сборка брала библиотеку из одного каталога, а связанные с ней инструменты — из другого. Конфигурация только предупреждала, и проблема проявилась гораздо позже, там, где причину искать было уже трудно.
Сторож, который ничего не сторожил. Валидатор одного из правил безопасности из-за ошибки в пути несколько недель проверял ноль файлов и каждый раз говорил OK. Проверка проходила именно потому, что не выполнялась.
Причина у всех трёх одна: у договорённости две или три стороны, а их совпадение держится на внимании людей.
При чём тут AI-агенты
Значительную часть кода у меня пишут агенты, в основном Claude Code и Codex. С межкомпонентными связями они справляются хуже людей.
Типичный сценарий: агент меняет Go-структуру, тесты зелёные, агент пишет «Готово» — и даже не открывает TypeScript-интерфейс на другой стороне.
Причины одни и те же:
Локальные правки. Вторая сторона шва лежит в другой папке, на другом языке, в другом процессе.
Ограниченный контекст. Агент не держит в голове весь продукт, а компилятор не скажет ему, что где-то есть клиент, зависящий от изменённой структуры.
Узкое «готово». Для агента это «все проверки, которые я вижу, прошли». Если ни одна не смотрит на шов, расхождение незаметно до поломки.
Зато агенты хорошо выполняют точные локальные инструкции. Если сразу после правки сказать, какой файл на другой стороне открыть и что там должно совпасть, агент это сделает. А если не отпускать его, пока стороны расходятся, ошибка не попадёт в коммит.
Идея: каждая связь сама говорит, как её проверять
Правило простое: каждая связь между компонентами объявляет, как её стороны согласуются. Если договорённость поддерживается вручную с обеих сторон, у неё должен быть сторож — проверка, которая читает обе стороны.
Связи описываются в JSON рядом с кодом. В карте есть блоки — программы, сервисы, хранилища, конфиги — и связи между ними: HTTPS, named pipe, файлы, генерация кода, переменные окружения. Каждое утверждение карты привязано якорем к конкретному литералу в коде: если код переехал, карта не продолжает молча описывать прошлое — проверка падает.
У каждой связи есть поле contract.enforcement:
Способ | Кто держит равенство сторон | Чего требует проверка |
|---|---|---|
| символ, который импортируют обе стороны |
|
| генератор из одного источника |
|
| общие тестовые векторы |
|
| никто, копия руками | сторож, читающий файл на каждой стороне |
| никто, договорённость | сторож, читающий файл на каждой стороне |
| никто | сторож, читающий файл на каждой стороне |
Уже сама классификация полезна: вопрос «где я полагаюсь на чью-то память?» превращается в список, который можно посчитать. Сторож, читающий одну сторону, не засчитывается: инструмент проверяет, что он читает файлы двух разных концов связи.
Три сторожа: join, set и pair
Сторожа намеренно простые: регулярные выражения, сравнение значений или множеств. Проверка занимает миллисекунды, не требует сборки и работает с любым языком.
join — значение совпадает везде. Версия API в TypeScript-клиенте и в Go-сервере:
{"id": "api-version", "link": "web-orders", "points": [ {"path": "web/src/api.ts", "side": "web", "regex": "API_VERSION = \"([^\"]+)\""}, {"path": "server/main.go", "side": "api", "regex": "APIVersion = \"([^\"]+)\""}]}
Сравниваются все вхождения, а не первое: в первой версии вторая копия значения ниже по файлу могла проскочить.
set — состав совпадает. Поля TypeScript-интерфейса и JSON-теги Go-структуры; статусы, которые пишет Python-воркер, и константы в Go; переменные окружения в коде и в .env.example. Намеренное различие вносится в allow с причиной, а ставшая ненужной запись allow сама роняет проверку.
pair — дублированная логика без одного значения. Например, округление цены на Go и TypeScript. Участки помечаются crossweft:begin price-rounding / crossweft:end price-rounding, Crossweft хэширует оба. Изменился любой — проверка падает, пока кто-то не перечитает двойника и не выполнит crossweft attest price-rounding --reason "...". Причина попадает в lock-файл и в ревью.
Самый короткий для агента путь из красной проверки — аттестовать изменённую сторону, не трогая вторую. Поэтому если с прошлой аттестации поменялся только один участок, attest отказывает: сначала приведите в порядок двойника, а если он действительно уже эквивалентен — скажите это явно флагом --other-side-unchanged.
Есть проверки крупнее. Маршруты сервера должны быть на карте — Go chi разбирается напрямую (а также Express и Fastify, FastAPI, Flask, gin и echo), остальное описывается регулярным выражением. Каждый вызов вида /v1/... в клиенте должен объясняться связью. Блок может отправлять данные, только если создал или получил их. Каждая папка с кодом должна быть на карте.
Если встроенных видов мало, репозиторий добавляет свои плагинами. У меня их 13: один сверяет 916 мест, где Go-сервер обращается к SQL, со 161 таблицей и 276 триггерами схемы, другой — 185 маршрутов сервера, третий — 27 пар «ожидающий ≥ работа + запас» для таймаутов. Плагин, который не импортировался, упал или ничего не сравнил, — ошибка, а не зелёный результат.
Что видит агент
Сама проверка — половина решения. Важно, чтобы агент узнал о шве сразу после правки.
Для этого три хука:
Старт сессии — короткое описание: в репозитории есть карта швов, сколько их и как работать.
После каждой правки — если файл лежит на шве, агенту называют вторую сторону и сторожа.
Перед завершением —
crossweft check; если шов расходится, хук не даёт закончить и объясняет, что делать.
После правки Go-сервера в демо агент видит:
crossweft: server/main.go is part of 3 cross-component seam(s). Keep both sides in agreement: - link web-orders (web -> api, https; Orders API [duplicated]): you changed its to side. Re-read: web/src/api.ts. Guarded by: join:api-version, join:version-header, set:order-fields, set:web-statuses, pair:price-rounding. ... Run `crossweft check` before you finish.
Поднял версию API в Go и пытается закончить — хук возвращает его:
crossweft check fails: - join:api-version disagrees: web@web/src/api.ts:3='2026-09-01'; api@server/main.go:14='2026-10-01' [server/main.go:14] (key: join:api-version:b5dfb7e5) A seam is out of agreement: bring the other side in line with the one you changed (`crossweft impact <file>` names it).
Совет зависит от вида проблемы: если в коде пропал литерал, на который указывает карта, агенту скажут «обнови якорь или верни код», а не «исправь другую сторону». Ключ заканчивается дайджестом того, какой файл держит какое значение: зарегистрированное расхождение прощает только себя.
У stop-хука две типичные беды — бесконечный цикл и тихий пропуск. Он возвращает агента, только пока тот продвигается (набор падающих ключей меняется), и не больше четырёх раз подряд. Нет прогресса — агент может остановиться, но человек получает явное сообщение, что проверка красная.
Для Claude Code есть плагин с хуками и skill. Для Codex — skill-плагин. Любой агент с поддержкой MCP может подключить crossweft mcp: проверка, анализ влияния правки и «какой файл перечитать» как инструменты только на чтение. Адаптеры хуков для Gemini CLI, Copilot и Cursor пока экспериментальные: проверена форма вывода, а не полный сценарий.
Проверки, которые не могут молча пройти
Третья история — валидатор, который неделями сканировал ноль файлов. Не каждое правило сводится к сравнению двух значений: иногда нужно доказать, что у константы один источник. Такие правила живут в маленьких скриптах, и именно они ломаются незаметно.
Мета-раннер crossweft validators считает валидатор провалившимся, если тот вернул ненулевой код, не вывел SCANNED: files=<n> items=<n> или вывел нули, не прошёл self-test с подложенной ошибкой (строка SELF-TEST: checks=<n>) или не уложился в таймаут. Честно неприменимый валидатор выводит APPLICABILITY: <причина> — это SKIP, а не PASS.
Тот же принцип у самого Crossweft. Три исхода, а не два: 0 — сошлось, 1 — расхождения, 2 — проверка не состоялась (модель не читается, ничего не просканировано). Код 2 никогда не выглядит как успех.
Реестр расхождений, который не гниёт
Не каждое расхождение можно исправить в том же PR. Тогда его записывают как finding: ключ, владелец, следующий шаг. Пока проблема воспроизводится, проверка проходит; когда её исправили, запись становится протухшей и сама роняет проверку. Сломанные якоря finding’ом не прикрыть: карта не имеет права описывать код неправильно.
Для внедрения в репозиторий, где расхождения уже накопились, есть crossweft baseline: он записывает их как открытые findings с владельцем. Проверка зеленеет, новый дрейф её роняет, а исправленное само требует закрытия.
Что это дало: цифры
С середины июля установщик ни разу не доводил системную службу до запуска на чистой виртуальной машине. Проверяемые швы я добавила 26–28 сентября, 29 сентября служба впервые запустилась. Это совпадение по времени, а не доказательство причины: параллельно чинилось и другое.
Ошибки все равно остались но их характер изменился. Три из первых падений 28 сентября были именно швами: код выхода 76 (имя службы в трёх копиях), отказ сервера, потому что загрузчик регистрировался по версии артефакта, а запрашивался по версии протокола, и HTTP 500, потому что два пути публикации убирали старый пакет, а третий — нет. Теперь каждое закрыто отдельным сторожем. Две следующие проблемы были уже обычными багами, а не «одним значением в нескольких местах».
Что показывает карта (пересчитано по истории репозитория):
27.09 | 29.09 | 4.10 | |
|---|---|---|---|
Блоки / связи | 130 / 160 | 134 / 166 | 138 / 192 |
join-сторожа | 10 | 220 (529 точек) | 261 (644 точки) |
set-сторожа | 4 | 125 | 138 |
Реестр расхождений | 58, все открыты | 106: 93 закрыто, 10 открыто, 3 приняты | 146: 117 закрыто, 25 открыто, 4 приняты |
Первый проход по правилу «у каждой связи объявлен способ согласования» сделали сами агенты: за день добавили 185 join- и 107 set-сторожей и нашли 15 настоящих расхождений в коде. Из 93 закрытых расхождений у 91 есть исправляющий коммит, и в 58 из них сообщение называет идентификатор из карты.
Карта без проверки устаревает за неделю. Первый замер через неделю показал 96 проблем, и 94 из них — устаревшая карта, а не баги кода. Поэтому карта проверяется на каждой остановке агента. Валидаторов стало 50 вместо 4 в июне, и каждый доказывает, что действительно что-то просканировал.
Выход в свет
Когда безопасность у меня наконец сошлась, я поняла: наверняка есть куча людей с такими же проблемами. У всех, кто пишет продукт на нескольких языках вместе с агентами, швы расходятся одинаково. Значит, это может пригодиться не только мне. Так Crossweft стал отдельным открытым инструментом: командой crossweft, плагином для Claude Code и Codex и сервером MCP для остальных агентов.
А разве такого ещё нет?
Каждая часть по отдельности существует. Сочетания и правила «каждая связь объявляет способ согласования, и каждый рукописный шов имеет сторожа» я не нашла.
Инструмент | Что делает | Чем отличается |
|---|---|---|
Google | «изменил блок — измени и тот файл» | проверяют, что дифф задел файл; join сравнивает значения на каждом прогоне |
равенство значения в разных файлах | нет множеств, участков кода и карты | |
контрактные тесты и совместимость схем | правильный ответ, если схема есть; Crossweft строит сторожей из OpenAPI и proto для рукописной стороны | |
ArchUnit, dependency-cruiser, import-linter | архитектурные правила | внутри одного языка |
архитектурные инварианты и skills для агентов | ближе всех по духу, но структура, а не значения между языками | |
граф кода и анализ влияния для агентов, MCP | граф для запросов, а не падающая проверка; дополняют друг друга | |
fiberplane/drift, Swimm | документация, привязанная к коду | хэш падает на любую правку; якорь — только когда исчез сам факт |
Подход придуман не мной. Бригитта Бёкелер описывает harness engineering: «направляющие» до того, как агент пишет код, и «датчики» после; детерминированные проверки против проверок языковой моделью. OpenAI пишет о том же в Harness engineering: leveraging Codex in an agent-first world. Crossweft — детерминированный датчик для межкомпонентных швов, чей вывод говорит агенту, что именно чинить.
Ограничения
Регулярки, а не разбор кода. Переформатировали объявление — проверка громко упадёт, регулярку придётся поправить. Но слишком широкий шаблон может пропустить реальное расхождение, поэтому значения стоит просматривать, а поведение проверять тестами.
Честность способа согласования проверяет человек. Агенты строят карту, Crossweft не даёт им придумать связь или пропустить папку, но
shared-codeэто или две копии — иногда знает только разработчик.Внедрение стоит труда. На маленьком проекте выгода невелика; окупается там, где языков и процессов много.
Пользователь пока один — я. Полностью проверена работа с Claude Code; остальные агенты — через MCP и skill, адаптеры их хуков экспериментальные.
check --changedускоряет pre-commit, но пока не тогда, когда правка задела роутер или клиент.
Попробовать
Быстрее всего — сломать демо (Go-API, TypeScript-клиент, Python-воркер):
pip install "git+https://github.com/AnastasiyaW/crossweft.git@v0.2.0" git clone --branch v0.2.0 --depth 1 https://github.com/AnastasiyaW/crossweft cd crossweft/examples/polyglot-shop && crossweft check # RESULT: PASS # поменяйте API_VERSION только в web/src/api.ts: crossweft check # join:api-version disagrees
Каждая строка таблицы проверена на копии демо: проверка падает с кодом 1 и называет нужного сторожа.
Изменение | Кто ловит |
|---|---|
поднять |
|
добавить JSON-поле только в Go-структуру |
|
добавить статус только в Python-воркер |
|
читать в воркере переменную, которой нет в |
|
вызвать |
|
изменить округление в |
|
В своём репозитории: crossweft init (init --example — чтобы сначала увидеть рабочий шов), дальше агент строит карту по skill, crossweft check его ведёт. В Claude Code: /plugin marketplace add AnastasiyaW/crossweft, затем /plugin install crossweft@crossweft. В CI: uses: AnastasiyaW/crossweft@v0.2.0 или crossweft check --format sarif для GitHub code scanning.
Репозиторий Crossweft охраняет собственные швы Crossweft’ом: версию в трёх файлах, события хуков, команды из документации.
Crossweft распространяется под Apache-2.0. Issues и pull requests — в github.com/AnastasiyaW/crossweft. Английская версия — happyin.work/blog/crossweft-seams.
