feat: recognize a payload projection in doctor and lint - #61
Conversation
Ревью нашло тихую порчу лока — исправлено в
|
Все 11 находок ревью закрыты в
|
| # | Находка | Как закрыто |
|---|---|---|
| 1 | TOCTOU: классификация проекции пере-проверялась по живой ФС при сборке лока | buildPlan возвращает множество спроецированных путей, предусловия берут его, а не пере-статят |
| 2 | upstream-удаление проекции роняло весь прогон | удаление сквозь проекцию — конфликт с указанием убрать файл из template/ |
| 3 | новый файл под спроецированным каталогом роняло прогон | Covers распознаёт путь под проекцией даже когда файла ещё нет → внятный конфликт |
| 4 | pull --plan не строился ни в одном репозитории с проекцией |
чтение назначения идёт через projection-aware обёртку |
| 5 | init при расходящейся проекции не создавал lock |
спецветка убрана, работает обычная логика adoption |
| 6 | одна ссылка на каталог подавляла вторую законную | цепочка предков вместо общего множества посещённых |
| 7 | ссылка на предка дублировала дерево под фантомным префиксом | цепочка предков засеяна корнем обхода |
| 8 | вложенность проверялась против обходимого дерева, а не репозитория | передаётся настоящий repoRoot |
| 9 | игнор по имени ссылки, а не по её цели | проверяется база разрешённой цели |
| 10 | governance молча не применялся под спроецированным каталогом | doctor спускается в него так же, как lint |
| 11 | doctor перерезолвливал путь после проверки |
используется путь, который вернул Resolve |
Каждая закреплена тестом. Добавились: удаление проекции, новый файл под каталогом, построение плана, adoption в init, adoption непрослеженной совпадающей проекции, четыре теста обхода lint, governance под проекцией.
Попутно нашлось и исправлено: непрослеженная проекция, чьё содержимое совпадает с входящим payload, давала конфликт «unmanaged file blocks managed template path». Проекция — не пользовательский файл, а указатель на payload, защищать там нечего; теперь она принимается как managed.
Проверка на dapi/memory-bank — pull даёт ровно один настоящий конфликт (.gitignore в корне шире шаблонного), doctor чист и при пофайловой, и при каталожной проекции, lint видит все 120 документов, обычный downstream-репозиторий не затронут.
Свойство безопасности сохранено и усилено формулировкой в CHANGELOG: в репозитории без template/ ни один путь не может быть проекцией, поэтому поведение байт-в-байт прежнее.
Второй раунд ревью закрыт в
|
| # | Находка | Как закрыто |
|---|---|---|
| 1 | Covers не видел ничего глубже одного уровня под спроецированным каталогом |
проход вверх продолжается мимо обычных каталогов, а не останавливается на первом |
| 2 | выполнение собственной рекомендации конфликта ломало следующий прогон | проверка проекции перенесена до чтения назначения; повисшая ссылка распознаётся по тексту (Declares); текст рекомендации теперь просит удалить и саму ссылку |
| 3 | спроецированный agent-файл ронял прогон с ELOOP | читатель согласован с inspectDestination; запись managed-блока сквозь проекцию заблокирована |
| 4 | governance.unsafe_symlink перестал срабатывать на ссылки наружу и повисшие |
обход сообщает о них вместо молчаливого пропуска |
| 5 | пропуск предусловия лока снимал проверку на момент коммита | пропуск убран целиком: проверка сверяет диск с digest входящего payload и работает как есть |
| 6 | регрессия: .md-ссылка наружу выпадала из корпуса |
восстановлено дореформенное поведение для файлов; containment требуется только для каталогов как корней обхода |
| 7 | в governance не было фильтра игнорируемых каталогов | фильтр по цели ссылки, как в lint |
| 8 | чтение проекции идёт мимо openat/pinned-root |
см. ниже |
| 9 | inspectDestination отдавал путь внутрь template/ как цель мутации |
возвращается сам путь назначения; резолвленный — только для чтения |
Про находку 6
Отдельно отмечу, потому что это единственное, что задевало репозитории без template/. До этого PR os.ReadFile следовал по ссылке и документ аудировался под своим внутренним путём; мой обход стал его терять, и каждая ссылка на него становилась битой. Мой же прежний тест закреплял это ошибочное поведение — удалён.
Про находку 8 — оставляю осознанно
Чтение проекции идёт readRegularFile(resolved) вместо openat-цепочки. Считаю это приемлемым и не хочу маскировать: Resolve уже проверил, что путь разрешается внутрь репозитория ровно в payload-файл, чтение не ведёт к записи, а для эксплуатации нужен локальный доступ на запись в репозиторий — при котором злоумышленник просто отредактирует template/ напрямую. Переписывание secure-траверса под «следовать по последней ссылке» — отдельная работа с собственным риском, и делать её мимоходом в этом PR неправильно.
Проверка на dapi/memory-bank после правок: pull — один настоящий конфликт, doctor чист при обеих формах проекции, lint видит все 120 документов, обычный downstream (lint и doctor) не затронут.
8d25d88 to
0a00318
Compare
Репозиторий, владеющий `template/`, не является downstream-установкой самого себя. Копировать payload в установленное дерево там незачем, поэтому такой репозиторий может представлять generic-документы симлинками в `template/`. `doctor` считал это дефектом: `manifest.managed_unreadable` и `governance.unsafe_symlink` на каждый такой документ. `lint` при этом те же файлы читал как обычные и не возражал — инструменты расходились между собой. `internal/projection` отвечает на один вопрос: разрешается ли путь внутри корня репозитория И указывает ли он ровно на payload-файл, стоящий за этим путём, в репозитории, который действительно несёт `template/memory-bank`. Симлинк наружу, симлинк на другой файл, битая ссылка и обычный файл проекцией не считаются и сообщаются как прежде. Объём сознательно минимален. Прошлые редакции этой ветки трогали мутационный путь и обход каталожных симлинков; три раунда ревью показали, что там своя поверхность риска — от тихой порчи lock до факториального роста числа документов при перекрёстных ссылках, причём обход менял поведение для всех репозиториев, а не только для тех, что несут payload. Обе части вынесены в feat/projection-ownership-support. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RtxeRwhVf1y8zvxUtk6xrS
Объём сокращён второй раз — и это ставит вопрос о нужности PRТретий раунд ревью дал 10 находок, и решающей оказалась не отдельная из них, а сводка в конце: изменения обхода применялись ко всем репозиториям, а не только к тем, что несут payload. Обе высокие находки — оттуда: ссылка на предка дублировала поддерево фантомом и давала ложные падения Хождение по каталожным симлинкам было нужно только ради каталожной формы проекции. В Осталось 283 строки, из них 96 — тесты: новый Попутно закрыты находки 8 и 10: признание проекции требует, чтобы репозиторий действительно нёс Но вот факт, который стоит обсудить до мержаПроверил старым, релизным CLI ту самую спроецированную ветку Причина простая: репозиторий-источник шаблона не держит lock, а без lock То есть после сужения у этого PR нет потребителя сегодня. Он остаётся честным исправлением несогласованности ( Решение о мерже или закрытии оставляю владельцу репозитория. Если закрывать — ветки |
0a00318 to
963513d
Compare
Closes #60 в части, которая нужна прямо сейчас; мутационная часть вынесена (см. ниже).
Суть
Репозиторий, владеющий
template/, не является downstream-установкой самого себя. Копировать payload в установленное дерево там незачем, поэтому такой репозиторий может представлять generic-документы симлинками вtemplate/— они равны payload по построению и не могут разойтись.CLI считал это дефектом, причём непоследовательно:
doctorвыдавалmanifest.managed_unreadableиgovernance.unsafe_symlinkна каждый такой документ, аlintте же файлы читал как обычные и не возражал.Что делает PR
internal/projectionотвечает на один вопрос: разрешается ли путь внутри корня репозитория И указывает ли он ровно на payload-файл, стоящий за этим путём.doctorчитает документ сквозь проверенную проекцию в обоих сканах;lintи governance-сканdoctorспускаются в каталожный симлинк, остающийся внутри репозитория.WalkDirв них не заходит, из-за чего содержимое молча выпадало из области, а ссылки на него объявлялись битыми.Попутно исправлено в обходе
lint: цепочка предков вместо общего множества посещённых (две ссылки на один каталог — два законных места, а не цикл), проверка вложенности против репозитория, а не обходимого дерева, и отказ по цели ссылки, а не по её имени.Защита не ослаблена
Симлинк наружу, симлинк на другой payload-файл, битая ссылка и обычный файл проекцией не считаются и сообщаются как прежде. Репозиторий без
template/не затронут: там ни один путь не может быть проекцией.Почему объём сокращён
Изначально PR трогал и мутационный путь —
pull,init, ownership lock. Два раунда ревью дали 20 находок, и их распределение оказалось показательным: всё опасное сидело в мутационной части (предусловия лока, secure-траверс, порядок проверок, тихая порча digest), всё безопасное и полезное — в чтении.При этом
dapi/memory-bank, ради которого всё делалось, от мутационной части не получает ничего: там больше нет lock иpullне запускается. Поэтому она вынесена вfeat/projection-ownership-support— со всеми исправлениями обоих раундов, чтобы работа не пропала, и с собственным ревью, когда до неё дойдут руки.initиpullэтим PR не затронуты. Репозиторий с проекцией не держит lock — чтоdoctor --profile templateи предписывает.Проверка на dapi/memory-bank
doctorна спроецированном инстансе с локомlintпри каталожной проекции (8 симлинков вместо 110)doctor --profile templateпри обеих формах проекцииlintиdoctor)🤖 Generated with Claude Code
https://claude.ai/code/session_01RtxeRwhVf1y8zvxUtk6xrS