Files
progcode/editorial/agent-rewrites/095.json
T
huncode 2d914b543f
Build and deploy / deploy (push) Failing after 15s
Publish rewritten technical article archive
2026-08-02 22:19:34 +03:00

8 lines
22 KiB
JSON
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.
{
"index": 95,
"slug": "editorial-2025-05-mechanism-engineering-automation",
"title": "Почему preview и approval не делают автоматизацию безопасной",
"excerpt": "Preview показывает намерение, approval фиксирует согласие, журнал хранит событие. Без ограничения scope и независимой проверки состояния автоматизация всё равно может изменить не те объекты.",
"contentHtml": "<p>Pipeline завершился зелёным, preview показал ожидаемый diff, а reviewer нажал approve. После запуска изменились записи за пределами заявки. В журнале есть operation id, но он не отвечает на главный вопрос: какие объекты реально изменились. Команда тратит часы на восстановление списка целей, проверку частичного результата и ручной откат.</p>\n<p>Цена ошибки растёт с размером batch-операции. Неверный selector меняет не одну запись, а весь совпавший набор. Старый preview уже не описывает состояние системы. Автоматический rollback может затереть исправления, которые кто-то внёс между двумя запусками.</p>\n<p>Тезис простой: безопасный change требует разных доказательств на разных границах. Preview доказывает только рассчитанное намерение. Authority ограничивает допустимый scope. Approval связывает согласие с конкретной версией proposal. Audit trail сохраняет переходы. Verification читает целевое состояние после execute. Если один слой подменяет другой, runner должен остановиться.</p>\n<h2>Механизм: пять артефактов, пять вопросов</h2>\n<p>У каждой фазы свой владелец и свой вопрос. Preview отвечает, что планировщик рассчитал в фиксированный момент. Authority отвечает, имеет ли операция право работать с выбранным scope. Approval отвечает, согласовал ли reviewer именно эту операцию. Audit trail отвечает, какое событие записал исполнитель. Verification отвечает, видит ли авторитетный reader ожидаемый результат.</p>\n<p>Ни один ответ не следует из другого. Наличие approval не доказывает, что reviewer видел полный список целей. Запись execution не доказывает, что target system приняла каждое изменение. Успешный dry-run не доказывает, что следующий write применится к тому же состоянию.</p>\n<div class=\"table-scroll\"><table><caption>Что на самом деле доказывает каждый артефакт</caption><thead><tr><th scope=\"col\">Артефакт</th><th scope=\"col\">Честное утверждение</th><th scope=\"col\">Опасная подмена</th><th scope=\"col\">Следующая проверка</th></tr></thead><tbody><tr><td>Preview</td><td>Планировщик рассчитал proposed change для выбранного snapshot</td><td>Execute будет ровно таким же</td><td>Пересчитать preconditions перед write</td></tr><tr><td>Authority</td><td>Policy разрешает selector и размер scope</td><td>Change полезен и технически корректен</td><td>Проверить intent и содержимое preview</td></tr><tr><td>Approval</td><td>Reviewer разрешил связанную карточку</td><td>Reviewer проверил каждую цель и побочный эффект</td><td>Сверить operation id и digests</td></tr><tr><td>Audit trail</td><td>Исполнитель записал событие по контракту</td><td>Target system уже находится в ожидаемом состоянии</td><td>Сделать независимый read</td></tr><tr><td>Verification</td><td>Reader увидел заданное условие</td><td>Все последствия change безопасны</td><td>Проверить остаточный риск и recovery</td></tr></tbody></table></div>\n<h2>Preview имеет срок годности</h2>\n<p>Preview — снимок намерения, а не разрешение на исполнение. Между расчётом и write другой job может изменить объект. Policy может обновиться. Selector может начать выбирать другой набор. Поэтому в карточке хранят не только человекочитаемый diff, но и operation id, snapshot version, selector, dense target list, target digest, expected precondition и proposal digest.</p>\n<p>Terraform разделяет speculative plan и план, который можно применить. Официальная документация прямо предупреждает: изменения в целевой системе после раннего speculative plan могут поменять финальный эффект, поэтому перед apply нужно проверить актуальный non-speculative plan. Этот принцип переносится на самописный runner без привязки к Terraform: старый diff нужно пересчитать или отклонить по TTL.</p>\n<p>Dry-run тоже бывает разным. В Kubernetes <code>kubectl apply --dry-run=client</code> только печатает объект и не отправляет его. Режим <code>--dry-run=server</code> отправляет server-side request, но не сохраняет ресурс. Первый проверяет локальную сериализацию. Второй проходит часть серверной обработки. Ни один режим не подтверждает будущий persistent change.</p>\n<h2>Authority ограничивает мощность</h2>\n<p>Authority не решает, стоит ли менять поле. Она ограничивает то, что исполнитель может сделать: допустимый selector, максимальное число целей, срок действия и исключения. Если policy разрешает два объекта, карточка с тремя не должна превращаться в warning. Runner возвращает stop до approval.</p>\n<p>Граница должна быть машинной. Сравнивайте exact selector и exact target digest. Не полагайтесь на текст «небольшой batch». Не уменьшайте список молча: reviewer должен увидеть тот же scope, который получит executor. Не расширяйте authority по умолчанию, если часть целей стала недоступна.</p>\n<h2>Учебный пример: остановка до write</h2>\n<p>Ниже — самостоятельная модель в памяти. Она не обращается к сети, не читает права и не меняет production. Карточка содержит три synthetic targets, а authority разрешает два. Отрицательный путь важнее счастливого: операция не доходит до approval.</p>\n<pre><code>const operation = {\n id: 'op-normalize-labels-v1',\n selector: 'labels.env=staging',\n targets: ['service-a', 'service-b', 'service-c'],\n targetDigest: 'targets-a-b-c-v1',\n proposalDigest: 'proposal-7f2a'\n};\n\nconst authority = {\n id: 'authority-l2-v1',\n selector: 'labels.env=staging',\n maxTargets: 2\n};\n\nfunction requestApproval(op, policy) {\n if (op.selector !== policy.selector) {\n return { status: 'STOP', reason: 'selector-not-authorized' };\n }\n if (op.targets.length > policy.maxTargets) {\n return {\n status: 'STOP',\n reason: 'scope-exceeds-authority-limit',\n targetCount: op.targets.length\n };\n }\n return { status: 'READY_FOR_APPROVAL', operationId: op.id };\n}\n\nconsole.log(requestApproval(operation, authority));\n// { status: 'STOP', reason: 'scope-exceeds-authority-limit', targetCount: 3 }</code></pre>\n<p>Пример проверяет контракт, а не безопасность пользователя. В реальной системе policy получает identity из провайдера, срок из доверенных часов, а target list — из авторитетного inventory. Здесь значения фиксированы, чтобы показать один переход: превышение лимита останавливает цепочку до любого внешнего действия.</p>\n<h2>Approval должен быть связанным</h2>\n<p>Approval без binding легко переносится на другую операцию. Минимальная связка содержит operation id, proposal digest, target digest, authority id, reviewer role, decision и expiry. Executor сравнивает эти поля перед write. Любое несовпадение, отсутствие или неизвестное значение возвращает stop.</p>\n<p>Похожий gate есть в GitHub Environments: job, который ссылается на environment, проходит настроенные protection rules до запуска и доступа к environment secrets. Required reviewers и bypass — отдельные настройки. Это gate стадии, а не доказательство смысла изменения. Reviewer подтверждает разрешение на job, но не заменяет проверку конкретного target digest.</p>\n<p>Проверяйте также отрицательный путь. Если preview относится к <code>targets-a-b-v1</code>, а approval к <code>targets-a-c-v1</code>, процесс не должен выбирать «более похожий» список. Он возвращает <code>STOP: approval-does-not-bind-target</code> и требует новый preview. Иначе система превратит человеческую ошибку в скрытый write.</p>\n<h2>Audit trail не наблюдает состояние</h2>\n<p>Журнал связывает фазы одной операции. В нём полезны operation id, version runner, selector, digests, authority, approval, timestamps и результат каждого target. Это помогает восстановить причинную цепочку. Но журнал фиксирует сообщение исполнительной системы. Он не читает целевой объект и не доказывает, что тот сохранился.</p>\n<p>Разделяйте receipt и evidence. Receipt говорит: «executor отправил запрос и получил ответ». Evidence говорит: «authoritative reader прочитал поле после операции». Если dashboard помечает change как verified только по наличию audit event, он скрывает самый дорогой отказ — неизвестное состояние.</p>\n<h2>Verification и rollback</h2>\n<p>Verification начинается с явного expected state. Например: reader видит у двух объектов label <code>env=staging</code>, target digest совпадает с карточкой, отсутствующие объекты перечислены, а stale inventory имеет отдельный статус. Exit code исполнителя не заменяет этот read.</p>\n<p>Если reader недоступен, состояние неизвестно. Если один объект не совпал, операция остановлена. Не переводите эти исходы в «успех с предупреждением», если следующий запуск может затронуть тот же scope.</p>\n<p>Rollback не является обратной строкой в <code>finally</code>. После частичного выполнения исходное значение может быть устаревшим. Часть объектов может исправить человек. Новая policy может запретить обратную операцию. Безопасный rollback начинается с observed state, нового bounded scope, новой authority, нового preview и нового approval. Исходное approval не наследуется.</p>\n<h2>Симптом → причина → проверка → действие</h2>\n<div class=\"table-scroll\"><table><caption>Диагностика автоматизированного change</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>После approve изменились лишние объекты</td><td>Scope не связан с approval или selector расширился</td><td>Сравнить target digest, selector и preconditions перед write</td><td>Остановить операцию и создать новый bounded preview</td></tr><tr><td>Preview зелёный, а execute получил другой diff</td><td>Между фазами изменился target state</td><td>Проверить snapshot version и возраст preview</td><td>Пересчитать plan; старый approval не использовать</td></tr><tr><td>В audit есть success, но поле не изменилось</td><td>Receipt выдали за verification</td><td>Прочитать authoritative source после execute</td><td>Вернуть статус unknown или mismatch</td></tr><tr><td>Dry-run прошёл, write отклонён сервером</td><td>Локальная проверка не видела server-side policy</td><td>Сравнить client/server режим и ответ admission</td><td>Исправить вход или остановить до нового review</td></tr><tr><td>Rollback снова повредил часть данных</td><td>Использовали старый scope и старое состояние</td><td>Собрать observed inventory и проверить preconditions</td><td>Планировать rollback как отдельный change</td></tr></tbody></table></div>\n<h2>Иллюстрация границ</h2>\n<figure><img src=\"/assets/editorial/2025/engineering-automation-2025-authority-matrix.svg\" alt=\"Матрица показывает связь authority, target scope, preview digest, approval binding, execution и verification; несовпадение останавливает цепочку\" loading=\"lazy\" /><figcaption>Authority ограничивает scope, но не оценивает смысл идеи. Approval связывает карточку. Verification проверяет состояние после execute. Красная клетка означает STOP, а не автоматический rollback.</figcaption></figure>\n<h2>Порядок действий</h2>\n<ol><li><strong>Опишите операцию.</strong> Запишите intent, selector, target list, target digest, expected before и expected after.</li><li><strong>Зафиксируйте границу preview.</strong> Укажите snapshot version, источник данных, время расчёта и условие устаревания.</li><li><strong>Проверьте authority.</strong> Сравните selector, лимит, срок и исключения до запроса approval.</li><li><strong>Свяжите approval.</strong> Сохраните operation id, proposal digest, target digest и authority id. Любое несовпадение остановите.</li><li><strong>Повторите preconditions перед write.</strong> Проверьте существование целей, версию, scope и действительность policy.</li><li><strong>Запишите receipt отдельно.</strong> Не называйте событие исполнителя доказательством состояния.</li><li><strong>Сделайте независимую verification.</strong> Прочитайте authoritative source и сравните его с expected state.</li><li><strong>Остановите неизвестное.</strong> При mismatch или недоступном reader соберите observed inventory и спланируйте новый bounded change. Не запускайте широкий rollback автоматически.</li></ol>\n<h2>Ограничения</h2>\n<p>Эта схема не создаёт identity provider, неизменяемое хранилище журнала, криптографическую подпись, распределенный lock или гарантию идемпотентности. Учебный код не читает реальные targets и не даёт production-результатов. Digests в примере — строки для проверки связи, а не доказательство криптографической стойкости.</p>\n<p>Независимая verification тоже имеет границу. Reader может быть устаревшим, неполным или не видеть побочные системы. Назначьте источник истины и отдельно опишите, что означает unknown. Для destructive change нужны дополнительные ограничения: backup, ручное подтверждение, лимит скорости и владелец recovery.</p>\n<p>Approval не делает решение правильным. Он только фиксирует разрешение в определённой роли. Authority не проверяет бизнес-смысл. Audit trail не гарантирует сохранность без свойств storage. Эти ограничения нужно оставить рядом с контрактом, иначе короткий статус начнёт обещать больше, чем проверка.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Механизм готов к ограниченному запуску, если другой инженер может по одной карточке восстановить intent, selector, exact targets, digests, authority, approval и expected verification. Тест с несовпадающим target digest останавливается до write. Устаревший preview требует пересчёта. Недоступный reader возвращает unknown. Частичный результат не запускает обратную операцию по старому approval.</p>\n<p>Минимальный набор проверок состоит из четырёх случаев: bounded scope проходит к review; scope выше лимита останавливается; approval с другим digest останавливается; после execute verification читает target system и отдельно сообщает mismatch. Только после прохождения этих случаев можно обсуждать более широкий scope. В статье нет утверждения, что такой механизм уже дал результат в production.</p>\n<h2>Проверяемые источники</h2><ul><li><a href=\"https://developer.hashicorp.com/terraform/cli/commands/plan\" target=\"_blank\" rel=\"noopener noreferrer\">HashiCorp Terraform: plan command</a> — официальная документация различает speculative plan и применяемый plan и предупреждает о расхождении после изменений в target system.</li><li><a href=\"https://kubernetes.io/docs/reference/kubectl/generated/kubectl_apply/\" target=\"_blank\" rel=\"noopener noreferrer\">Kubernetes: kubectl apply</a> — официальная reference различает client и server dry-run: один не отправляет объект, другой отправляет запрос без сохранения ресурса.</li><li><a href=\"https://docs.github.com/en/actions/how-tos/deploy/configure-and-manage-deployments/manage-environments\" target=\"_blank\" rel=\"noopener noreferrer\">GitHub Docs: managing environments for deployment</a> — официальная документация описывает protection rules, required reviewers и возможность запретить bypass; эти правила ограничивают запуск, но не доказывают корректность change.</li></ul>"
}