diff --git a/editorial/agent-rewrites/095.json b/editorial/agent-rewrites/095.json index 53bf304..37522ad 100644 --- a/editorial/agent-rewrites/095.json +++ b/editorial/agent-rewrites/095.json @@ -2,6 +2,6 @@ "index": 95, "slug": "editorial-2025-05-mechanism-engineering-automation", "title": "Почему preview и approval не делают автоматизацию безопасной", - "excerpt": "Preview показывает намерение, approval фиксирует согласие, журнал хранит событие. Без ограничения scope и независимой проверки состояния автоматизация всё равно может изменить не те объекты.", - "contentHtml": "
Pipeline завершился зелёным, preview показал ожидаемый diff, а reviewer нажал approve. После запуска изменились записи за пределами заявки. В журнале есть operation id, но он не отвечает на главный вопрос: какие объекты реально изменились. Команда тратит часы на восстановление списка целей, проверку частичного результата и ручной откат.
\nЦена ошибки растёт с размером batch-операции. Неверный selector меняет не одну запись, а весь совпавший набор. Старый preview уже не описывает состояние системы. Автоматический rollback может затереть исправления, которые кто-то внёс между двумя запусками.
\nТезис простой: безопасный change требует разных доказательств на разных границах. Preview доказывает только рассчитанное намерение. Authority ограничивает допустимый scope. Approval связывает согласие с конкретной версией proposal. Audit trail сохраняет переходы. Verification читает целевое состояние после execute. Если один слой подменяет другой, runner должен остановиться.
\nУ каждой фазы свой владелец и свой вопрос. Preview отвечает, что планировщик рассчитал в фиксированный момент. Authority отвечает, имеет ли операция право работать с выбранным scope. Approval отвечает, согласовал ли reviewer именно эту операцию. Audit trail отвечает, какое событие записал исполнитель. Verification отвечает, видит ли авторитетный reader ожидаемый результат.
\nНи один ответ не следует из другого. Наличие approval не доказывает, что reviewer видел полный список целей. Запись execution не доказывает, что target system приняла каждое изменение. Успешный dry-run не доказывает, что следующий write применится к тому же состоянию.
\n| Артефакт | Честное утверждение | Опасная подмена | Следующая проверка |
|---|---|---|---|
| Preview | Планировщик рассчитал proposed change для выбранного snapshot | Execute будет ровно таким же | Пересчитать preconditions перед write |
| Authority | Policy разрешает selector и размер scope | Change полезен и технически корректен | Проверить intent и содержимое preview |
| Approval | Reviewer разрешил связанную карточку | Reviewer проверил каждую цель и побочный эффект | Сверить operation id и digests |
| Audit trail | Исполнитель записал событие по контракту | Target system уже находится в ожидаемом состоянии | Сделать независимый read |
| Verification | Reader увидел заданное условие | Все последствия change безопасны | Проверить остаточный риск и recovery |
Preview — снимок намерения, а не разрешение на исполнение. Между расчётом и write другой job может изменить объект. Policy может обновиться. Selector может начать выбирать другой набор. Поэтому в карточке хранят не только человекочитаемый diff, но и operation id, snapshot version, selector, dense target list, target digest, expected precondition и proposal digest.
\nTerraform разделяет speculative plan и план, который можно применить. Официальная документация прямо предупреждает: изменения в целевой системе после раннего speculative plan могут поменять финальный эффект, поэтому перед apply нужно проверить актуальный non-speculative plan. Этот принцип переносится на самописный runner без привязки к Terraform: старый diff нужно пересчитать или отклонить по TTL.
\nDry-run тоже бывает разным. В Kubernetes kubectl apply --dry-run=client только печатает объект и не отправляет его. Режим --dry-run=server отправляет server-side request, но не сохраняет ресурс. Первый проверяет локальную сериализацию. Второй проходит часть серверной обработки. Ни один режим не подтверждает будущий persistent change.
Authority не решает, стоит ли менять поле. Она ограничивает то, что исполнитель может сделать: допустимый selector, максимальное число целей, срок действия и исключения. Если policy разрешает два объекта, карточка с тремя не должна превращаться в warning. Runner возвращает stop до approval.
\nГраница должна быть машинной. Сравнивайте exact selector и exact target digest. Не полагайтесь на текст «небольшой batch». Не уменьшайте список молча: reviewer должен увидеть тот же scope, который получит executor. Не расширяйте authority по умолчанию, если часть целей стала недоступна.
\nНиже — самостоятельная модель в памяти. Она не обращается к сети, не читает права и не меняет production. Карточка содержит три synthetic targets, а authority разрешает два. Отрицательный путь важнее счастливого: операция не доходит до approval.
\nconst 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 }\nПример проверяет контракт, а не безопасность пользователя. В реальной системе policy получает identity из провайдера, срок из доверенных часов, а target list — из авторитетного inventory. Здесь значения фиксированы, чтобы показать один переход: превышение лимита останавливает цепочку до любого внешнего действия.
\nApproval без binding легко переносится на другую операцию. Минимальная связка содержит operation id, proposal digest, target digest, authority id, reviewer role, decision и expiry. Executor сравнивает эти поля перед write. Любое несовпадение, отсутствие или неизвестное значение возвращает stop.
\nПохожий gate есть в GitHub Environments: job, который ссылается на environment, проходит настроенные protection rules до запуска и доступа к environment secrets. Required reviewers и bypass — отдельные настройки. Это gate стадии, а не доказательство смысла изменения. Reviewer подтверждает разрешение на job, но не заменяет проверку конкретного target digest.
\nПроверяйте также отрицательный путь. Если preview относится к targets-a-b-v1, а approval к targets-a-c-v1, процесс не должен выбирать «более похожий» список. Он возвращает STOP: approval-does-not-bind-target и требует новый preview. Иначе система превратит человеческую ошибку в скрытый write.
Журнал связывает фазы одной операции. В нём полезны operation id, version runner, selector, digests, authority, approval, timestamps и результат каждого target. Это помогает восстановить причинную цепочку. Но журнал фиксирует сообщение исполнительной системы. Он не читает целевой объект и не доказывает, что тот сохранился.
\nРазделяйте receipt и evidence. Receipt говорит: «executor отправил запрос и получил ответ». Evidence говорит: «authoritative reader прочитал поле после операции». Если dashboard помечает change как verified только по наличию audit event, он скрывает самый дорогой отказ — неизвестное состояние.
\nVerification начинается с явного expected state. Например: reader видит у двух объектов label env=staging, target digest совпадает с карточкой, отсутствующие объекты перечислены, а stale inventory имеет отдельный статус. Exit code исполнителя не заменяет этот read.
Если reader недоступен, состояние неизвестно. Если один объект не совпал, операция остановлена. Не переводите эти исходы в «успех с предупреждением», если следующий запуск может затронуть тот же scope.
\nRollback не является обратной строкой в finally. После частичного выполнения исходное значение может быть устаревшим. Часть объектов может исправить человек. Новая policy может запретить обратную операцию. Безопасный rollback начинается с observed state, нового bounded scope, новой authority, нового preview и нового approval. Исходное approval не наследуется.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| После approve изменились лишние объекты | Scope не связан с approval или selector расширился | Сравнить target digest, selector и preconditions перед write | Остановить операцию и создать новый bounded preview |
| Preview зелёный, а execute получил другой diff | Между фазами изменился target state | Проверить snapshot version и возраст preview | Пересчитать plan; старый approval не использовать |
| В audit есть success, но поле не изменилось | Receipt выдали за verification | Прочитать authoritative source после execute | Вернуть статус unknown или mismatch |
| Dry-run прошёл, write отклонён сервером | Локальная проверка не видела server-side policy | Сравнить client/server режим и ответ admission | Исправить вход или остановить до нового review |
| Rollback снова повредил часть данных | Использовали старый scope и старое состояние | Собрать observed inventory и проверить preconditions | Планировать rollback как отдельный change |
Эта схема не создаёт identity provider, неизменяемое хранилище журнала, криптографическую подпись, распределенный lock или гарантию идемпотентности. Учебный код не читает реальные targets и не даёт production-результатов. Digests в примере — строки для проверки связи, а не доказательство криптографической стойкости.
\nНезависимая verification тоже имеет границу. Reader может быть устаревшим, неполным или не видеть побочные системы. Назначьте источник истины и отдельно опишите, что означает unknown. Для destructive change нужны дополнительные ограничения: backup, ручное подтверждение, лимит скорости и владелец recovery.
\nApproval не делает решение правильным. Он только фиксирует разрешение в определённой роли. Authority не проверяет бизнес-смысл. Audit trail не гарантирует сохранность без свойств storage. Эти ограничения нужно оставить рядом с контрактом, иначе короткий статус начнёт обещать больше, чем проверка.
\nМеханизм готов к ограниченному запуску, если другой инженер может по одной карточке восстановить intent, selector, exact targets, digests, authority, approval и expected verification. Тест с несовпадающим target digest останавливается до write. Устаревший preview требует пересчёта. Недоступный reader возвращает unknown. Частичный результат не запускает обратную операцию по старому approval.
\nМинимальный набор проверок состоит из четырёх случаев: bounded scope проходит к review; scope выше лимита останавливается; approval с другим digest останавливается; после execute verification читает target system и отдельно сообщает mismatch. Только после прохождения этих случаев можно обсуждать более широкий scope. В статье нет утверждения, что такой механизм уже дал результат в production.
\nСимптом обнаруживается после зелёного pipeline: preview показал ожидаемый diff, а reviewer нажал approve. После запуска изменились записи за пределами заявки. В журнале есть operation id, но из него не видно, какие объекты реально изменились. Команда тратит часы на восстановление списка целей и рискует запустить ещё один широкий change под видом rollback.
\nПричина не в отсутствии ещё одной кнопки подтверждения. Разные этапы отвечают на разные вопросы: preview описывает предложение в определённый момент, authority задаёт предел полномочий, approval фиксирует согласие, audit trail сохраняет событие, а verification читает целевое состояние после записи. Без явной связи между ними зелёный статус доказывает меньше, чем кажется.
\nНачните с разделения доказательств. Это не бюрократия, а способ не выдать одно наблюдение за другое. Наличие preview не говорит, что scope разрешён. Approval не говорит, что target list остался прежним. Receipt от executor не доказывает, что целевая система сохранила значение.
\n| Артефакт | Честное утверждение | Опасная подмена | Следующая проверка |
|---|---|---|---|
| Preview | Для зафиксированного snapshot рассчитан proposed change | Execute даст тот же эффект | Проверить свежесть и preconditions перед записью |
| Authority | Policy разрешает selector и размер scope | Change полезен и технически корректен | Сверить intent и содержимое preview |
| Approval | Reviewer разрешил связанную карточку в своей роли | Согласие переносится на похожую операцию | Сравнить operation, proposal, target и authority digests |
| Audit trail | Executor записал событие по своему контракту | Target system уже в ожидаемом состоянии | Прочитать authoritative source после execute |
| Verification | Reader наблюдал заданное expected state | Все побочные последствия устранены | Проверить residual risk и план recovery |
В карточке операции храните как минимум идентификатор, selector, точный список targets, digest списка, ожидаемое состояние до и после, версию логики изменения и срок действия. Человеческое описание остаётся полезным для review, но executor должен сравнивать машинные поля буквально.
\nPreview устаревает, когда меняется объект, inventory или policy. Поэтому его ценность ограничена snapshot version и временем расчёта. Если между preview и execute другой job обновил target, старый diff больше не является описанием текущего действия. В безопасном процессе есть явное условие: пересчитать предложение или остановиться, если precondition больше не выполняется.
\nЭто соответствует модели Terraform. Официальная документация описывает terraform plan как расчёт изменений без их выполнения. План без -out является speculative plan и не содержит намерения применяться. Документация отдельно предупреждает, что изменения в target system между ранним speculative plan и финальным применением могут изменить результат; перед apply нужно снова проверить актуальный non-speculative plan. Это узкий факт о Terraform, а не готовая гарантия для любого самописного runner.
Сохраняйте не только красивый diff. Практический минимум выглядит так: operationId, snapshotVersion, selector, dense target list, targetDigest, expected precondition и proposalDigest. Digest здесь связывает две фазы и позволяет обнаружить расхождение. Он не заменяет криптографическую подпись, проверку identity или защиту хранилища.
Слово dry-run описывает режим, но не обещает одинаковый объём проверки. В официальной reference для kubectl apply режим --dry-run=client только печатает объект, который был бы отправлен, без отправки. Режим --dry-run=server передаёт запрос на сервер, но не сохраняет ресурс. Значит, client mode проверяет локальную подготовку объекта, а server mode проходит часть серверного пути. Ни один режим сам по себе не подтверждает будущий persistent change.
В карточке записывайте источник данных и границу побочного эффекта: «локальная модель, внешний read не выполняется», «server admission, сохранение запрещено» или «read-only inventory со snapshot». Тогда reviewer понимает, какую проверку ещё нужно сделать. Если этого поля нет, слово preview создаёт ложное ощущение полноты.
\nAuthority — это машинная граница: допустимый selector, максимальное число targets, срок действия и исключения. Она отвечает на вопрос «можно ли этой роли работать с таким scope», но не решает, правильно ли менять поле. Если policy разрешает два объекта, карточка с тремя должна остановиться до approval. Список нельзя молча уменьшать: reviewer должен видеть ровно тот scope, который получит executor.
\nСравнивайте exact selector и exact target digest. Не подменяйте их словами «маленький batch» или «почти тот же список». Не расширяйте authority, если часть целей исчезла из inventory. Не превращайте превышение лимита в warning, когда последующая операция может затронуть больше объектов, чем видел reviewer.
\nНиже — автономный пример на Node.js. Он работает только с объектами в памяти, не читает сеть, не проверяет реальные права и ничего не меняет во внешней системе. В нём три synthetic target, authority разрешает два, а approval связан с точным operation и target digest. Скопируйте блок в shell с Node.js 18+:
\nnode --input-type=module <<'NODE'\nconst 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\nconst approval = {\n operationId: 'op-normalize-labels-v1',\n targetDigest: 'targets-a-b-c-v1',\n proposalDigest: 'proposal-7f2a',\n authorityId: 'authority-l2-v1'\n};\n\nfunction gate(op, policy, decision) {\n const reasons = [];\n if (op.selector !== policy.selector) reasons.push('selector-not-authorized');\n if (op.targets.length > policy.maxTargets) reasons.push('scope-exceeds-authority-limit');\n if (decision.operationId !== op.id\n || decision.targetDigest !== op.targetDigest\n || decision.proposalDigest !== op.proposalDigest\n || decision.authorityId !== policy.id) {\n reasons.push('approval-does-not-bind-operation');\n }\n return reasons.length === 0\n ? { status: 'READY_FOR_EXECUTE', operationId: op.id }\n : { status: 'STOP', reasons };\n}\n\nconsole.log(gate(operation, authority, approval));\n// { status: 'STOP', reasons: [ 'scope-exceeds-authority-limit' ] }\n// No external target is read or changed.\nNODE\nКоманда возвращает STOP, потому что три цели превышают лимит два. Чтобы проверить вторую отрицательную ветку, замените в approval значение targetDigest на targets-a-c-v1: результат дополнится approval-does-not-bind-operation. В настоящей системе digest вычисляет канонизатор данных, authority приходит из доверенного policy engine, а identity и срок решения проверяются отдельно.
Approval должен содержать operation id, proposal digest, target digest и authority id. Полезны также роль reviewer, decision, reason и expiry. Executor сравнивает эти значения перед write; «похожий» selector или частичное совпадение не подходят. Если approval относится к targets A и C, а preview — к A и B, процесс возвращает stop и требует новый preview.
\nУ GitHub Environments есть похожая, но более узкая граница: job, который ссылается на environment, должен пройти настроенные protection rules до запуска или доступа к environment secrets. В документации required reviewers и запрет административного bypass описаны как отдельные настройки. Это пример gate запуска, а не доказательство корректности diff. Доступность функций зависит от типа репозитория и плана GitHub, поэтому переносить его поведение в собственный runner без проверки нельзя.
\nBypass тоже является событием с владельцем, причиной и audit record. Если исключение скрыто внутри общего поля approved=true, расследование не отличит обычное согласие от обхода. Для destructive operation отсутствие reviewer или истёкший expiry должны вести к stop, а не к default-решению.
Audit trail восстанавливает причинную цепочку: operation id, версии runner и policy, selector, digests, authority, approval, время и результат по каждой цели. Но запись «executor отправил запрос» — это receipt. Она не подтверждает, что target system приняла значение, что controller не откатил его сразу и что побочный объект не изменился.
\nVerification начинается с ожидаемого состояния. Например: authoritative reader видит label env=staging на двух конкретных targets, digest списка совпадает, а отсутствующие объекты перечислены. У проверки должны быть три различимых исхода: matched, mismatched и unknown. Недоступный reader — это unknown, а не «успех с предупреждением».
| Симптом | Гипотеза | Проверка | Действие |
|---|---|---|---|
| Изменились лишние объекты | Selector расширился или scope не был связан с approval | Сравнить target digest до write и фактический список | Остановить цепочку и собрать bounded inventory |
| Preview и execute показывают разный diff | Target state изменился между фазами | Сверить snapshot version, precondition и возраст preview | Пересчитать preview, старое approval не использовать |
| В audit есть success, но поле прежнее | Receipt выдали за verification | Прочитать authoritative source отдельным путём | Вернуть mismatched или unknown |
| Client dry-run прошёл, сервер отказал | Локальная проверка не видела server-side policy | Сравнить режим dry-run и ответ admission | Исправить вход или остановиться до нового review |
| Rollback снова меняет не те targets | Обратный write использует старый scope | Проверить observed state и новую authority | Оформить rollback как новую операцию |
После частичного результата нельзя считать исходное значение безопасной обратной командой. Человек мог исправить часть targets, исходное состояние могло стать устаревшим, а новая policy может запретить обратную запись. Поэтому первый шаг — observed inventory: что изменилось, где состояние неизвестно, какие зависимости затронуты и кто владеет recovery.
\nЗатем rollback проходит тот же bounded маршрут: новый scope, новые preconditions, новая authority, новый preview, отдельный approval и последующая verification. Статус «rollback planned» не означает «rollback executed». Автоматический inverse в блоке finally опасен именно потому, что исполняется без свежего знания о состоянии.
Схема не создаёт identity provider, policy engine, неизменяемое хранилище журнала, распределённую блокировку, гарантию идемпотентности или реальный rollback. Учебный код не доказывает права пользователя, свежесть inventory, криптографическую стойкость digest и фактическое состояние production. Он проверяет только связь полей и отказ до внешней записи.
\nДаже независимая verification имеет границу: reader может быть устаревшим, неполным или не видеть побочные системы. Для каждого change назовите источник истины, допустимую задержку и владельца unknown. Для удаления и других необратимых действий добавьте отдельные меры: резервную копию, лимит скорости, ручное подтверждение и план восстановления.
\nДокументация Terraform, Kubernetes и GitHub меняется; флаги и доступность функций нужно сверять с версией и тарифом своего окружения. Примеры в статье иллюстрируют узкие свойства официальных инструментов, а не сертифицируют единый безопасный процесс. Готовность определяется собственными тестами: scope выше лимита останавливается, чужой digest не проходит, недоступный reader остаётся unknown, а старое approval не запускает rollback.
\nplan, speculative plan, сохранённый план через -out и предупреждение о повторной проверке после изменений в target system.--dry-run=client и --dry-run=server, включая отсутствие сохранения ресурса в server mode.Скрипт меняет label на двух объектах и отрабатывает за секунду. Через неделю тот же selector находит две тысячи объектов. Job завершается со статусом success, но один шаблон не подходит части targets. В журнале есть время запуска и имя оператора, а список фактически изменённых объектов восстановить нельзя.
Симптом тихий: preview показывает только число объектов, approval хранит approved: true, а исполнитель повторно вычисляет динамический selector во время запуска. Цена ошибки — массовое неверное состояние, ручное восстановление, спор о границе операции и потеря времени команды. Если часть объектов успела измениться, старый input уже не описывает безопасный rollback.
Тезис: безопасная автоматизация строится не вокруг одной кнопки, а вокруг цепочки связанных доказательств. Preview отвечает, что предлагается. Ограниченный scope отвечает, какие targets допустимы. Approval разрешает именно этот scope. Audit trail сохраняет переходы. Verification проверяет наблюдаемое состояние после исполнения. При разрыве связи процесс останавливается.
Массовая операция начинается с намерения, но не должна сразу получать право записи. Сначала система строит operation card: идентификатор, selector, точный список targets, digest списка, ожидаемый переход и лимит размера. Затем отдельные проверки связывают карточку с authority и approval.
У каждого барьера свой вопрос:
Эти ответы нельзя склеивать. Наличие preview не доказывает будущий результат. Approval не доказывает, что reviewer проверил смысл каждого изменения. Receipt исполнителя не доказывает состояние target system. Лог не делает операцию обратимой.
Хороший preview показывает не фразу «обновить конфигурацию», а exact selection и proposed diff. Для списка targets нужны плотный массив идентификаторов, selector, exclusions и digest. Для изменения нужны expected before и expected after. Для расследования нужны operation id, версия правила и время построения.
Если selector вычисляется заново после approval, approval может относиться к другому набору. Поэтому исполнитель принимает только карточку с тем же operationId, targetDigest, selector и authority id. Любое несовпадение даёт stop. Список без digest можно прочитать, но его нельзя надёжно связать с последующим запросом.
У Terraform есть та же полезная граница: terraform plan создаёт execution plan и сам не выполняет предложенные изменения. Официальная документация отдельно предупреждает, что между speculative plan и применением состояние цели может измениться, поэтому перед применением нужен повторный non-speculative plan. Это пример того, почему preview нельзя считать вечным разрешением.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| В preview есть только число targets | Операция не фиксирует exact selection и exclusions | Сравнить список ids, selector и target digest | Остановить approval до появления списка и лимита |
| Job получил approval, но selector вычисляется снова | Approval не связан с preview | Проверить operation id, selector и digest перед execute | Отклонить запрос при любом несовпадении |
| В журнале есть success, но состояние неизвестно | Receipt исполнителя приняли за observed state | Сделать независимое чтение authoritative source | Оставить статус stopped или verification-pending |
| Rollback запускает обратный payload автоматически | Старый preview используют как новое разрешение | Проверить observed state и новый scope | Создать отдельный rollback plan и новую authority |
| Операция затрагивает лишние targets | Лимит проверяют только в интерфейсе | Подать scope больше лимита и проверить отказ до approval | Перенести проверку max targets в executor |
Ниже — учебный пример в памяти. Он не читает targets, не отправляет запросы и не выполняет запись. Его задача — показать границу между preview, approval, receipt и verification. В реальной системе объекты должны получать identity, durable storage, policy evaluation и контроль доступа из конкретной инфраструктуры.
const preview = { operationId: 'op-demo-17', selector: 'label=legacy', targetIds: ['doc-a', 'doc-b'], targetDigest: 'sha256:targets-a-b' }; const authority = { authorityId: 'role-maintainer', selector: 'label=legacy', maxTargets: 2 }; function requestApproval(p, a) { if (p.targetIds.length > a.maxTargets) return { status: 'STOP', reason: 'scope-exceeds-authority-limit' }; if (p.selector !== a.selector) return { status: 'STOP', reason: 'selector-not-allowed' }; return { status: 'WAITING_APPROVAL', operationId: p.operationId }; } function execute(approved, p) { if (approved.operationId !== p.operationId || approved.targetDigest !== p.targetDigest) return { status: 'STOP', reason: 'approval-does-not-bind-preview' }; return { status: 'SIMULATED_RECEIPT', effect: 'учебная запись' }; }Первый отказ должен произойти до approval, если карточка содержит третий target. Второй — до simulated execute, если кто-то подменил digest после согласования. Отсутствующее поле, неизвестный operation id и лишнее поле также должны закрывать путь. Fail-closed означает, что неполный или подозрительный input не получает безопасный-looking default.
SIMULATED_RECEIPT — только запись о прохождении учебной функции. Она не означает, что doc-a и doc-b изменились. Проверка результата потребовала бы отдельного чтения системы, которой в примере нет.
Preview и dry-run часто называют одним словом, хотя они отвечают за разные слои. Preview доменной операции может построить proposed diff из локальной модели. Dry-run конкретной команды может остановиться на клиенте или отправить проверяемый запрос серверу без сохранения.
Документация Kubernetes для kubectl apply разделяет режимы --dry-run=client и --dry-run=server. Client только печатает объект, который был бы отправлен. Server отправляет запрос без persistence ресурса. Значит, статус «dry-run прошёл» нужно сопровождать указанием режима, источника данных и границы. Ни один режим не гарантирует, что поздний реальный запрос встретит те же policy и состояние.
Если проверка использовала client dry-run, нельзя утверждать, что API-сервер примет запрос. Если server dry-run прошёл, нельзя утверждать, что через час selector вернёт тот же набор ресурсов. Следующее действие — повторить preconditions рядом с execute и сохранить новый digest.
Поле approved: true слишком слабое. Минимальная связь включает operation id, preview digest, target digest, authority id, reviewer или service identity, policy version и срок действия. Если digest другой, согласие относится к другой операции. Если authority другой, неизвестно, какой лимит применялся. Если истёк срок, старое решение не должно продолжать жить.
GitHub Actions даёт официальный пример узкой контрольной точки: job, который ссылается на environment с required reviewers, ждёт approval до старта; доступ к environment secrets появляется только после прохождения protection rules. Это контроль допуска к запуску. Он не доказывает правильность diff, качество selector или факт изменения целевых объектов. Содержательный review остаётся отдельной проверкой.
Audit trail связывает переходы: operation id, preview digest, authority, approval, actor, время, receipt и stop reason. Он помогает восстановить последовательность без поиска по чату и stdout. Но свойства «append-only» и «невозможно подделать» нельзя получить одним названием поля. Их нужно обеспечивать конкретным хранилищем, правами записи, retention и проверкой целостности.
Verification начинается после execute и использует authoritative read. Сначала задайте expectation: например, на каждом target поле label должно иметь значение current, а число отсутствующих targets равно нулю. Затем укажите источник, допустимую задержку и правило частичного результата. Если source недоступен, status должен быть verification-pending, а не verified.
Зелёный exit code не заменяет observed state. Receipt сообщает, что executor дошёл до своей точки завершения. Verification сообщает, что внешний объект сейчас выглядит ожидаемым образом. Эти записи нельзя объединять в одно поле.
Автоматический inverse payload опасен. Target мог измениться вручную после запуска. Исходное значение могло быть неправильным. Часть объектов могла принять новое состояние, а часть — нет. Внешняя зависимость могла изменить порядок восстановления.
При остановке безопасно создать только rollback plan: сохранить причину, прочитать observed state, определить новый bounded scope, выбрать owner, построить новый preview и получить новую authority и approval. Статус rollback-planned не означает rollback-executed. Старое approval не даёт права на новый write.
Учебная модель не проверяет реальные permissions, identity provider, состояние базы, гонки между preview и execute, сетевые повторы, durable audit storage, scheduler, очереди или фактический rollback. Она также не доказывает, что selector выражает правильное бизнес-условие. Все значения кода фиксированы в памяти; production effect намеренно не выполняется.
Даже exact digest не решает проблему сам по себе. Хэш связывает представления, но не говорит, что selection полон, authority разумна, expected state корректен или reviewer понял риск. Нужны владельцы данных, политика доступа и источник истины. Чем дороже ошибка, тем меньше должна быть граница первой операции.
Операция готова к ограниченному реальному запуску, если другой инженер может без устного объяснения восстановить exact targets, proposed diff, maximum scope, authority, approval binding, stop conditions, receipt и authoritative verification. Тест с подменой target digest останавливает executor до записи. Тест с лишним target останавливает approval до запуска. После execute существует отдельная проверка observed state. При несовпадении система не делает скрытый retry и создаёт только новый rollback plan.
Если хотя бы один пункт держится на внимательности оператора, автоматизацию не расширяют. Сначала переносят правило в contract и проверяют отрицательный путь. Практическая граница — не «job завершился успешно», а «система показывает, что именно было разрешено, что произошло и каким чтением проверен результат».
Скрипт меняет label на двух документах и завершается за секунду. Через неделю тот же selector находит две тысячи объектов. Job получает статус success, но часть targets не подходит под новое правило.
Проблема возникает не из-за синтаксиса. Preview показывает только число объектов, approval хранит approved: true, а исполнитель заново вычисляет динамический selector после согласования. В журнале остаются время и имя оператора, но точный список изменений восстановить нельзя.
Безопасная автоматизация — это цепочка доказательств: что выбрано, что разрешено, что отправлено и что наблюдается после операции. Если связь между этими состояниями разорвана или evidence устарело, путь записи закрывается.
Массовая операция начинается с намерения, но не должна сразу получать право записи. Сначала система строит operation card: идентификатор, selector, точный список targets, digest списка, ожидаемый переход и лимит размера. Затем отдельные проверки связывают карточку с authority и approval.
У каждого барьера свой вопрос:
Эти ответы нельзя склеивать. Наличие preview не доказывает будущий результат. Approval не доказывает, что reviewer проверил смысл каждого изменения. Receipt исполнителя не доказывает состояние target system. Лог не делает операцию обратимой.
Хороший preview показывает не фразу «обновить конфигурацию», а exact selection и proposed diff. Для списка targets нужны плотный массив идентификаторов, selector, exclusions и digest. Для изменения нужны expected before и expected after. Для расследования нужны operation id, версия правила и время построения.
Если selector вычисляется заново после approval, approval может относиться к другому набору. Поэтому исполнитель принимает только карточку с тем же operationId, targetDigest, selector и authority id. Любое несовпадение даёт stop. Список без digest можно прочитать, но его нельзя надёжно связать с последующим запросом.
У Terraform есть та же полезная граница: terraform plan создаёт execution plan и сам не выполняет предложенные изменения. Официальная документация отдельно предупреждает, что между speculative plan и применением состояние цели может измениться, поэтому перед применением нужен повторный non-speculative plan. Это пример того, почему preview нельзя считать вечным разрешением.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| В preview есть только число targets | Операция не фиксирует exact selection и exclusions | Сравнить список ids, selector и target digest | Остановить approval до появления списка и лимита |
| Job получил approval, но selector вычисляется снова | Approval не связан с preview | Проверить operation id, selector и digest перед execute | Отклонить запрос при любом несовпадении |
| В журнале есть success, но состояние неизвестно | Receipt исполнителя приняли за observed state | Сделать независимое чтение authoritative source | Оставить статус stopped или verification-pending |
| Rollback запускает обратный payload автоматически | Старый preview используют как новое разрешение | Проверить observed state и новый scope | Создать отдельный rollback plan и новую authority |
| Операция затрагивает лишние targets | Лимит проверяют только в интерфейсе | Подать scope больше лимита и проверить отказ до approval | Перенести проверку max targets в executor |
Ниже — учебный пример в памяти. Он не читает targets, не отправляет запросы и не выполняет запись. Его задача — показать границу между preview, approval, receipt и verification. В реальной системе отдельно задайте identity, durable storage, policy evaluation и контроль доступа; этот пример намеренно остаётся моделью в памяти.
node --input-type=module <<'EOF'\nimport { createHash } from 'node:crypto';\n\nconst canonicalTargets = (ids) => [...new Set(ids)].sort();\nconst digestTargets = (ids) => createHash('sha256')\n .update(JSON.stringify(canonicalTargets(ids)))\n .digest('hex');\n\nconst preview = {\n operationId: 'op-demo-17',\n selector: 'label=legacy',\n targetIds: ['doc-b', 'doc-a'],\n targetDigest: digestTargets(['doc-b', 'doc-a']),\n};\nconst authority = {\n authorityId: 'role-maintainer',\n selector: 'label=legacy',\n maxTargets: 2,\n};\n\nfunction approve(p, a) {\n const targets = canonicalTargets(p.targetIds);\n if (targets.length !== p.targetIds.length) {\n return { status: 'STOP', reason: 'duplicate-target' };\n }\n if (targets.length === 0 || targets.length > a.maxTargets) {\n return { status: 'STOP', reason: 'scope-exceeds-authority-limit' };\n }\n if (p.selector !== a.selector) {\n return { status: 'STOP', reason: 'selector-not-allowed' };\n }\n if (p.targetDigest !== digestTargets(p.targetIds)) {\n return { status: 'STOP', reason: 'preview-digest-mismatch' };\n }\n return {\n status: 'APPROVED',\n operationId: p.operationId,\n targetDigest: p.targetDigest,\n authorityId: a.authorityId,\n };\n}\n\nfunction execute(p, approval) {\n if (approval.status !== 'APPROVED'\n || approval.operationId !== p.operationId\n || approval.targetDigest !== p.targetDigest) {\n return { status: 'STOP', reason: 'approval-does-not-bind-preview' };\n }\n return { status: 'SIMULATED_RECEIPT', operationId: p.operationId };\n}\n\nconst approval = approve(preview, authority);\nconsole.log(approval);\nconsole.log(execute(preview, approval));\nconsole.log(execute(preview, {\n ...approval,\n targetDigest: digestTargets(['doc-a', 'doc-c']),\n}));\nEOFПервый отказ должен произойти до approval, если карточка содержит третий target. Второй — до simulated execute, если кто-то подменил digest после согласования. Отсутствующее поле, неизвестный operation id и лишнее поле также должны закрывать путь. Fail-closed означает, что неполный или подозрительный input не получает безопасный-looking default.
SIMULATED_RECEIPT — только запись о прохождении учебной функции. Она не означает, что doc-a и doc-b изменились. Проверка результата потребовала бы отдельного чтения системы, которой в примере нет.
Preview и dry-run часто называют одним словом, хотя они отвечают за разные слои. Preview доменной операции может построить proposed diff из локальной модели. Dry-run конкретной команды может остановиться на клиенте или отправить проверяемый запрос серверу без сохранения.
Документация Kubernetes для kubectl apply разделяет режимы --dry-run=client и --dry-run=server. Client только печатает объект, который был бы отправлен. Server отправляет запрос без persistence ресурса. Значит, статус «dry-run прошёл» нужно сопровождать указанием режима, источника данных и границы. Ни один режим не гарантирует, что поздний реальный запрос встретит те же policy и состояние.
Если проверка использовала client dry-run, нельзя утверждать, что API-сервер примет запрос. Если server dry-run прошёл, нельзя утверждать, что через час selector вернёт тот же набор ресурсов. Следующее действие — повторить preconditions рядом с execute и сохранить новый digest.
Поле approved: true слишком слабое. Минимальная связь включает operation id, preview digest, target digest, authority id, reviewer или service identity, policy version и срок действия. Если digest другой, согласие относится к другой операции. Если authority другой, неизвестно, какой лимит применялся. Если истёк срок, старое решение не должно продолжать жить.
GitHub Actions даёт официальный пример узкой контрольной точки: job, который ссылается на environment с required reviewers, ждёт approval до старта; доступ к environment secrets появляется только после прохождения protection rules. Это контроль допуска к запуску. Он не доказывает правильность diff, качество selector или факт изменения целевых объектов. Содержательный review остаётся отдельной проверкой.
Audit trail связывает переходы: operation id, preview digest, authority, approval, actor, время, receipt и stop reason. Он помогает восстановить последовательность без поиска по чату и stdout. Но свойства «append-only» и «невозможно подделать» нельзя получить одним названием поля. Их нужно обеспечивать конкретным хранилищем, правами записи, retention и проверкой целостности.
Verification начинается после execute и использует authoritative read. Сначала задайте expectation: например, на каждом target поле label должно иметь значение current, а число отсутствующих targets равно нулю. Затем укажите источник, допустимую задержку и правило частичного результата. Если source недоступен, status должен быть verification-pending, а не verified.
Зелёный exit code не заменяет observed state. Receipt сообщает, что executor дошёл до своей точки завершения. Verification сообщает, что внешний объект сейчас выглядит ожидаемым образом. Эти записи нельзя объединять в одно поле.
Автоматический inverse payload опасен. Target мог измениться вручную после запуска. Исходное значение могло быть неправильным. Часть объектов могла принять новое состояние, а часть — нет. Внешняя зависимость могла изменить порядок восстановления.
При остановке безопасно создать только rollback plan: сохранить причину, прочитать observed state, определить новый bounded scope, выбрать owner, построить новый preview и получить новую authority и approval. Статус rollback-planned не означает rollback-executed. Старое approval не даёт права на новый write.
Учебная модель не проверяет реальные permissions, identity provider, состояние базы, гонки между preview и execute, сетевые повторы, durable audit storage, scheduler, очереди или фактический rollback. Она также не доказывает, что selector выражает правильное бизнес-условие. Все значения кода фиксированы в памяти; реальная запись намеренно не выполняется.
Даже exact digest не решает проблему сам по себе. Хэш связывает представления, но не говорит, что selection полон, authority разумна, expected state корректен или reviewer понял риск. Нужны владельцы данных, политика доступа и источник истины. Чем дороже ошибка, тем меньше должна быть граница первой операции.
Операция готова к ограниченному реальному запуску, если другой инженер может без устного объяснения восстановить exact targets, proposed diff, maximum scope, authority, approval binding, stop conditions, receipt и authoritative verification. Тест с подменой target digest останавливает executor до записи. Тест с лишним target останавливает approval до запуска. После execute существует отдельная проверка observed state. При несовпадении система не делает скрытый retry и создаёт только новый rollback plan.
Если хотя бы один пункт держится на внимательности оператора, автоматизацию не расширяют. Сначала переносят правило в contract и проверяют отрицательный путь. Практическая граница — не «job завершился успешно», а «система показывает, что именно было разрешено, что произошло и каким чтением проверен результат».
node:crypto и createHash, использованных в учебном примере.Инженер копирует в AI-чат фрагмент ошибки, удаляет имя пользователя и ждёт подсказку. Симптом исчезает из текста, но риск остаётся. В логе могут сохраниться время операции, редкий тип события, идентификатор запроса, структура платежа или сочетание полей, по которому запись узнают. Если сервис добавляет к prompt историю диалога, файлы проекта или поиск, наружу уходит не только выделенная строка.
\nЦена ошибки складывается из нескольких частей. Команда теряет контроль над копией данных. Нельзя быстро доказать, куда она попала и как долго хранилась. Нельзя уверенно ответить на запрос владельца данных. В худшем случае один удобный ответ превращается в инцидент, расследование и остановку инструмента для всей команды.
\nБезопасный AI-review начинается не с маскирования и не с выбора модели. Сначала нужно доказать, что конкретный фрагмент можно передать конкретной поверхности по конкретному маршруту. Для этого разделите четыре решения: какой класс у данных, какие условия обработки действуют, куда пойдёт запрос и кто имеет право его отправить. Если хотя бы один факт неизвестен, путь должен закрыться.
\nЭта граница важнее названия продукта. Один vendor может иметь несколько поверхностей: веб-чат, расширение редактора, API, поиск и агент с доступом к репозиторию. Они могут собирать разный контекст и использовать разные настройки хранения. Одобрение «AI разрешён» не покрывает автоматически каждый экран и каждый тип записи.
\nВходной фрагмент обычно рассматривают как строку. Система должна рассматривать его как объект с контекстом: recordId, версией, владельцем, классом, целью, destination и сроком действия разрешения. Redaction меняет содержимое. Он не назначает класс и не подтверждает договорные условия. Поле, заменённое на [redacted], всё ещё может быть чувствительным по структуре.
Следующий слой — маршрут. Запрос может пройти через прокси, региональный endpoint, подключённый поиск, плагин или сервис-посредник. Успешное TLS-соединение доказывает только доступность канала. Оно не доказывает, что выбранный destination разрешён для этого класса данных. Так же firewall allow-list не превращает restricted record в public.
\nПоследний слой — полномочия. Разрешение относится к ресурсу, цели, requester и сроку. Старое согласование для синтетического примера не даёт права отправить production-лог. Доступ к инструменту не равен праву передавать ему все доступные пользователю данные.
\nНиже приведён синтетический пример. Он не читает лог, не содержит PII, секретов, клиентских идентификаторов и настоящего endpoint. Названия намеренно фиксированы. Цель примера — показать отрицательный путь: при restricted-классе функция закрывает передачу до проверки разрешения. Это не готовая DLP-система и не доказательство безопасности какого-либо AI-сервиса.
\nconst request = {\n recordId: 'synthetic-log-shape-v1',\n dataClass: 'restricted',\n allowExternalEgress: false,\n destination: 'fixed-external-ai-boundary-alpha',\n requester: 'synthetic-engineering-read',\n expiresAt: '2025-04-20T00:00:00Z'\n};\n\nfunction decide(request, now) {\n if (request.dataClass === 'restricted') {\n return { decision: 'stop', reason: 'class-denies-external-egress' };\n }\n\n if (!request.allowExternalEgress) {\n return { decision: 'stop', reason: 'contract-not-proven' };\n }\n\n if (new Date(request.expiresAt) <= now) {\n return { decision: 'stop', reason: 'authorization-expired' };\n }\n\n return { decision: 'hand-off' };\n}\n\nconst result = decide(request, new Date('2025-04-19T12:00:00Z'));\n// { decision: 'stop', reason: 'class-denies-external-egress' }\nПорядок проверок здесь принципиален. Сначала система видит запрет класса. Она не использует наличие пользователя или действующий срок как override. Если заменить класс на допустимый, нужно всё равно проверить условия хранения, destination, requester и дату. Положительный результат в таком fixture означает только, что фиксированные входы соответствуют фиксированным правилам учебной модели.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| «Мы удалили имена, значит всё можно» | Redaction перепутали с классификацией | Проверить остаточную структуру, owner и правило класса | Остановить передачу до решения владельца данных |
| «Продукт уже разрешён» | Surface, plan или route отличаются | Зафиксировать фактический destination и дополнительные источники context | Сверить конкретный маршрут с policy и договором |
| «Сеть пропускает endpoint» | Технический egress приняли за право на данные | Сопоставить data class, destination и условия обработки | Не отправлять payload при любом несовпадении |
| «Есть approval от команды» | Разрешение не имеет scope или истекло | Проверить record, requester, цель, issuer и expiresAt | Запросить новое датированное решение или выбрать локальный путь |
| «Ассистент ответил, значит утечки нет» | Проверили output, но не весь outbound path | Проверить request, журналы, включённые интеграции и retention terms | Считать результат недоказанным до проверки маршрута |
На схеме нет шага «попросить модель оценить риск». Модель может помочь сформулировать вопрос, но не должна сама становиться источником разрешения на доступ к данным. Сначала работает детерминированный gate. Он возвращает один из двух исходов: hand-off в заранее разрешённый workflow или stop с причиной и владельцем следующего решения.
\nЭта схема не заменяет инвентаризацию данных, DLP, договор, privacy impact assessment, контроль доступа и аудит поставщика. Она не обнаруживает PII сама по себе. Она не знает, что делает vendor после получения запроса, если это не подтверждено условиями конкретного сервиса. Она также не решает вопрос законного основания обработки и не определяет допустимость данных без владельца policy.
\nМаскирование может снизить объём данных, но не гарантирует анонимизацию. Локальная модель может убрать внешний egress, но не отменяет права доступа и хранение локальных журналов. Человеческое согласование полезно, но не должно заменять проверяемые ограничения в коде и конфигурации.
\nУчебные строки в примере не дают production-результата. Они показывают форму контракта. В реальной системе положительный результат нужно подтвердить документацией поставщика, конфигурацией маршрута, журналом события и решением владельца данных на дату запуска.
\nРешение готово, если другой инженер без доступа к исходному обсуждению может повторить проверку и получить тот же исход. Для одного synthetic restricted record тест должен показать: внешний вызов не выполнен, payload не попал в prompt builder, причина stop сохранена, а следующий владелец назван. Для допустимого учебного record тест должен показать все совпадения: class, условия обработки, destination, requester и expiry.
\nЕсли хотя бы один из этих фактов нельзя предъявить ссылкой, конфигурацией или тестовым результатом, передача не доказана. Оставьте данные локально и закройте вопрос до появления недостающего evidence.
\nИнженер копирует в AI-чат фрагмент ошибки, заменяет имя на [redacted] и ждёт подсказку. На вид в тексте больше нет персональных данных. Но рядом могут остаться время операции, редкий тип события, идентификатор запроса, структура платежа или сочетание полей, по которому запись легко узнать. А сама поверхность может добавить к запросу историю диалога, файл проекта, поиск или другой контекст.
Симптом здесь не в том, что AI дал плохой ответ. Проблема обнаруживается позже: команда не может доказать, какой фрагмент ушёл, к какому сервису, на каких условиях и с чьего разрешения. Цена такой неопределённости — повторная инвентаризация, остановка полезного сценария и риск раскрытия персональных, финансовых или служебных данных. Маскирование уменьшает содержимое, но не превращает его автоматически в разрешённый контекст.
\nРассмотрим учебную ситуацию. Разработчик хочет показать ассистенту форму production-события и один тикет, чтобы найти причину повторной ошибки. В статье нет настоящего лога, клиента, секрета, идентификатора, файла или сетевого адреса. Вместо них используются фиксированные строки с пометкой synthetic. Это позволяет проверить границу решения, не передавая наружу даже учебный payload.
Первое ложное упрощение звучит так: «имена удалены, значит данные обезличены». Для этого вывода нужно отдельно проверить остаточную структуру и риск повторной идентификации. Второе: «у нас корпоративный тариф, значит разрешён любой экран». У одного поставщика могут отличаться web-чат, расширение редактора, API, поиск и агент с доступом к репозиторию. Третье: «домен разрешён firewall, значит передачу одобрили». Сеть отвечает за маршрут, а не за классификацию записи и права человека.
\nПеред отправкой полезно описывать не строку, а одну единицу контекста. У неё есть идентификатор, версия, владелец, класс, назначение, destination, requester и срок действия решения. Если поле неизвестно, его нельзя заполнять оптимистичным значением public. Нулевое знание должно вести к остановке.
| Слой | Вопрос | Минимальное доказательство | Чего он не доказывает |
|---|---|---|---|
| Класс | Что представляет запись после преобразования? | Версия классификации, owner и описание остаточной структуры | Условия хранения у поставщика и право на egress |
| Policy и contract | Допустим ли этот класс для выбранной surface? | Документ и версия условий для конкретного plan, региона и функции | Что фактически попало в запрос и кто его отправляет |
| Маршрут | Куда может уйти запрос и дополнительный context? | Surface, destination, proxy или gateway и техническое правило | Безопасность payload и полномочия requester |
| Полномочия | Кто может использовать именно эту запись сейчас? | Scoped approval: record, цель, requester, issuer и expiry | Разрешение всех будущих фрагментов и интерфейсов |
Эти слои пересекаются в одной операции, но не заменяют друг друга. Даже проверенный договор не классифицирует конкретный лог. Даже одобренный класс не открывает неизвестный endpoint. Даже доступ к AI-инструменту не означает право раскрывать все данные, которые видит пользователь. Итоговое решение должно ссылаться на все четыре слоя или закрывать путь.
\nRedaction — это преобразование значения. Классификация отвечает на другой вопрос: какой риск остаётся у результата. Например, замена номера клиента на [redacted] не скрывает редкую последовательность действий, точное время и вид операции. Небольшой набор таких признаков может сузить поиск до одной записи. Поэтому в карточке нужно фиксировать не только удалённые поля, но и то, что осталось.
Не следует и автоматически называть результат анонимным. Статья не предлагает юридический тест анонимизации: его критерии зависят от применимого права, целей обработки и возможностей сопоставления. Инженерная граница проще: пока владелец данных не подтвердил класс и допустимость, фрагмент не отправляется. Для отладки лучше начать с заранее подготовленного примера, схемы события или локального fixture, если именно они отвечают на вопрос.
\nСетевой allow-list полезен, но его область действия узкая. Он может разрешить соединение с определённым destination или направить его через прокси. Он не анализирует смысл каждого поля, не знает срок действия approval и не решает, разрешена ли выбранная surface условиями организации. TLS подтверждает защищённость канала от подмены при передаче, но не делает саму передачу допустимой.
\nТакой разрыв хорошо согласуется с принципом zero trust из NIST SP 800-207: доверие не должно следовать только из сетевого расположения, а authentication и authorization — отдельные функции перед доступом к ресурсу. Для AI-review это означает: факт «с рабочего ноутбука домен открывается» нельзя использовать как замену проверке requester, record и назначения.
\nНиже — автономный Node.js-фрагмент. Он создаёт две синтетические карточки в памяти, проверяет класс, destination, requester, approval и срок, а затем печатает решение. Код не читает файлы, не строит prompt и не выполняет HTTP-запрос. Его можно сохранить в временный файл и запустить командой из блока; ожидаемые строки показывают только поведение локального правила.
\nnode --input-type=module <<'NODE'\nconst policy = {\n allowedClass: 'synthetic-public-summary',\n destination: 'fixed-ai-review-boundary',\n requester: 'synthetic-engineering-read',\n issuer: 'synthetic-data-steward',\n now: '2025-04-19T12:00:00Z',\n};\n\nfunction decide(item) {\n const fields = ['recordId', 'dataClass', 'destination', 'requester', 'approvedBy', 'expiresAt'];\n if (fields.some((field) => typeof item[field] !== 'string' || item[field] === '')) {\n return { decision: 'stop', reason: 'incomplete-record' };\n }\n if (item.dataClass !== policy.allowedClass) {\n return { decision: 'stop', reason: 'class-not-allowed' };\n }\n if (item.destination !== policy.destination || item.requester !== policy.requester) {\n return { decision: 'stop', reason: 'scope-mismatch' };\n }\n if (item.approvedBy !== policy.issuer || Date.parse(item.expiresAt) <= Date.parse(policy.now)) {\n return { decision: 'stop', reason: 'approval-missing-or-expired' };\n }\n return { decision: 'review-ready', externalCall: false };\n}\n\nconst restricted = {\n recordId: 'synthetic-log-shape-v1',\n dataClass: 'synthetic-restricted',\n destination: policy.destination,\n requester: policy.requester,\n approvedBy: policy.issuer,\n expiresAt: '2025-04-20T00:00:00Z',\n};\nconst allowed = { ...restricted, recordId: 'synthetic-public-summary-v1', dataClass: policy.allowedClass };\n\nconsole.log(decide(restricted)); // { decision: 'stop', reason: 'class-not-allowed' }\nconsole.log(decide(allowed)); // { decision: 'review-ready', externalCall: false }\nNODE\nВажен не сам набор строк, а порядок. Запрет класса срабатывает до использования approval как исключения. Для допустимой учебной карточки результат означает лишь «можно передать evidence в следующий согласованный шаг». Поле externalCall: false намеренно подтверждает, что программа не отправляет ничего наружу. В рабочем сервисе вместо строк понадобятся реальные адаптеры к каталогу данных, identity-провайдеру и policy-as-code, но они не должны менять fail-closed смысл: неизвестное условие не становится разрешением.
| Наблюдение | Гипотеза | Проверка | Действие |
|---|---|---|---|
| Имена удалены | Остаточная структура безопасна | Сверить class после преобразования, редкие поля и owner | При неизвестном классе остановить передачу |
| Продукт разрешён | Все его поверхности одинаковы | Назвать точные feature, plan, region и destination | Сверить условия именно этой surface |
| Firewall пропускает домен | Технический egress равен праву на данные | Сопоставить route с class и policy | Не использовать сеть как approval |
| Есть согласование команды | Оно покрывает любой контекст | Проверить record, цель, requester, issuer и expiry | Запросить scoped решение или остаться локально |
| Ассистент ответил | Внешнего раскрытия не было | Проверить outbound request, дополнительные context sources и retention terms | Считать результат недоказанным до проверки маршрута |
Таблица нужна не для бюрократии. Она не даёт одному удачному факту закрыть соседний вопрос. Если команда не может назвать destination, это отдельный пробел маршрута. Если destination известен, но нет scoped approval, это отдельный пробел полномочий. Такой разбор уменьшает спор о бренде инструмента и показывает, кому адресовать следующий вопрос.
\nУчебный код не является DLP, системой классификации или юридическим заключением. Он не обнаруживает PII, не проверяет реальный endpoint, не знает договор конкретного поставщика, не измеряет retention, не контролирует права в identity-системе и не доказывает, что vendor сделал после получения запроса. Synthetic labels в примере не описывают ни одну настоящую организацию.
\nИсточник может подтвердить принцип, но не ваше разрешение. NIST AI RMF — добровольная рамка управления рисками; она помогает разложить вопросы по функциям Govern, Map, Measure и Manage, но не заменяет договор и локальную policy. OWASP перечисляет чувствительную информацию и меры снижения риска, но не принимает решение за владельца данных. Поэтому перед реальным запуском нужно повторно закрепить версию документа, plan, region, surface, destination и исключения.
\nЛокальная модель уменьшает внешний egress, но не отменяет контроль доступа и журналы на вашей машине. Маскирование снижает объём, но не гарантирует анонимизацию. Человеческое approval полезно только вместе с ограниченным scope и сроком; устное «можно AI» нельзя переносить на соседний record.
\nReview можно считать завершённым, когда другой инженер повторяет его без доступа к исходному обсуждению. Для restricted-карточки тест должен показать stop до вызова клиента, отсутствие payload в prompt builder, сохранённую причину и назначенного владельца следующего вопроса. Для разрешённой учебной карточки должны совпасть class, условия, destination, requester, issuer и expiry. Это не подтверждает правду о vendor; это подтверждает, что ваша локальная процедура не принимает неизвестное за известное.
\nПрактический следующий шаг — взять один тип рабочего контекста, но не копировать его в новый документ. Заполните пустую карточку четырьмя блоками: class и owner; policy и contract; маршрут; полномочия и срок. Затем намеренно оставьте один блок пустым и убедитесь, что workflow останавливается без payload. Если причина и следующий владелец не появляются в результате, сначала исправьте review-контур.
\nСимптом обычно выглядит безобидно: инженер копирует в AI-инструмент кусок лога, тикет или stack trace, а потом не может точно сказать, что именно ушло наружу. В настройках включён корпоративный тариф, сеть разрешает нужный домен, коллега устно подтвердил «можно». Но эти факты не доказывают одно и то же. Цена ошибки — раскрытие персональных данных или секрета, нарушение договорного условия, невозможность восстановить решение и остановка всего сценария до повторной проверки.
\nТезис статьи простой: передача контекста допустима только тогда, когда сходятся четыре независимых доказательства. Нужно знать класс записи, применимую policy и договорные условия, конкретный технический маршрут и полномочия человека на эту запись в этот срок. Если один слой неизвестен, система должна остановить hand-off и вернуть причину. Нельзя заменить отсутствующий факт более удобным фактом из другой колонки.
\nКласс описывает содержимое. Например, public, internal или restricted. Это не название папки и не ощущение автора фрагмента. Класс должен иметь владельца и правило, по которому его присвоили. Удаление имени из строки тоже не меняет автоматически класс: структура события, редкий идентификатор и сочетание полей могут оставаться чувствительными.
Policy отвечает на вопрос «допустима ли такая обработка». Договор уточняет условия для конкретного сервиса, тарифа, региона и функции: retention, обучение моделей, subprocessors или запрет внешнего хранения. Слово «корпоративный» не заменяет проверку этих условий. Одобрение продукта не является автоматически разрешением на любой payload.
\nEgress отвечает на другой вопрос: куда и каким путём может уйти запрос. Это endpoint, proxy, firewall, DNS-политика или другой сетевой control. Разрешённый маршрут не делает данные допустимыми. Authorization отвечает ещё на один слой: какой владелец разрешил конкретную запись, конкретному requester, для конкретного destination и до какой даты.
\nРассмотрим запрос на анализ ошибки в платёжном сервисе. Инженер хочет передать AI фрагмент журнала. В нём есть время, тип операции, идентификатор клиента и текст исключения. Даже если значение идентификатора заменили на client-17, нужно отдельно решить, что осталось в записи и какой класс ей присвоен.
Первое доказательство — карточка контекста. В ней есть стабильный идентификатор записи, версия, класс, владелец, срок действия и ссылка на правило классификации. Второе — policy и contract evidence для выбранной поверхности. Ссылка должна покрывать именно тот plan и тот режим, в котором работает инструмент. Третье — проверенный egress scope: destination и технический control должны совпадать с карточкой. Четвёртое — датированное решение владельца. Оно содержит record id, requester, destination, срок и причину.
\nПроверка не должна начинаться с approval. Иначе человек сможет невольно «перекрыть» запрет класса. Она не должна начинаться с сети. Иначе доступный endpoint станет ложным доказательством допустимости. Сначала проверяют форму запроса и запись, затем допустимость, маршрут, доступ и срок полномочий. Только после этого можно передать результат в отдельный разрешённый workflow.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| «В логе нет имени, значит можно» | Редактура смешана с классификацией | Сверить остаточную структуру, класс и owner rule | Не передавать; запросить классификацию или заменить запись на публичный пример |
| «У нас Enterprise-тариф» | План принят за договорное условие | Проверить policy, retention и training terms для нужной функции | Закрыть hand-off до появления ссылки и применимого scope |
| «Firewall уже разрешил домен» | Сетевой control принят за решение о данных | Сопоставить destination, route и egress scope с карточкой | Использовать только подтверждённый маршрут; не искать обход |
| «Коллега разрешал это раньше» | Approval не привязан к record, requester или сроку | Сверить id записи, роль, requester, destination и expiry | Получить новое датированное решение или остановить передачу |
| «Неизвестно, что вернёт интеграция» | Дополнительный context и внешние запросы не учтены | Проверить документацию конкретной surface и включённые функции | Оставить данные локально до подтверждения всех outbound paths |
Ниже — ограниченный пример на JavaScript. В нём используются только фиксированные строки, нет настоящего лога, PII, секрета, файла, сети, вызова модели или production side effect. Функция не отправляет payload. Она показывает только контракт: запись с запрещённым классом должна остановиться до проверки approval.
\nconst request = createFixedContextRequest('restricted-log-shape');\nconst decision = assessEgress(request);\nconst result = stopWhenClosed(decision);\n\nconsole.log(decision.accepted); // false\nconsole.log(decision.reason); // class-not-allowed\nconsole.log(result.payloadReleased); // false\nconsole.log(result.nextAction); // ask-data-owner\n\n// В этом учебном примере нет HTTP-вызова.\nРеальная реализация должна проверять строгую схему входа. Не принимайте лишние поля, отсутствующий scope, поддельную authorization или просроченную дату как частично корректный запрос. Fail-closed означает конкретный результат: accepted: false, причина, ссылка на проверенный источник и следующий владелец вопроса. Это не доказывает, что технология всегда запрещена. Это доказывает только, что текущих фактов недостаточно для данной передачи.
Документация конкретного AI-продукта может описывать обработку prompt, добавление repository context или отдельный внешний поиск. Это помогает обнаружить дополнительные границы egress. Но такая документация не классифицирует ваш лог и не выдаёт сотруднику право на его раскрытие.
\nСетевое правило снижает риск неправильного endpoint, но не видит смысл payload. DPA и условия сервиса помогают ответить на вопрос о permitted processing, но не подтверждают, что человек имеет доступ к записи. NIST описывает policy-based access и неявное доверие к network location как разные вещи. Эти источники формируют вопросы для проверки; они не дают универсального вердикта «можно передавать».
\nЧетыре проверки не заменяют DLP, data inventory, юридическую оценку, access control или технический мониторинг. Классификация может ошибиться. Документ поставщика может измениться. Proxy может быть настроен не так, как написано в схеме. Поэтому в рабочей системе храните версию evidence, дату проверки, применимый scope и границу, которую документ не покрывает.
\nНе обещайте результат, которого не измеряли. Эта статья не утверждает, что конкретная интеграция сохраняет или не сохраняет prompt, не обучает на нём модели и не гарантирует отсутствие утечки. Учебный код не является доказательством свойств production. Его проверяемый результат уже: запрещённый класс не приводит к передаче.
\nКонтроль готов к ограниченному применению, когда другой инженер может взять одну карточку без payload и воспроизвести весь вопрос: какой класс, какая policy и договорная версия, какой destination и route, кто разрешил, какой срок. Для каждого пробела система возвращает accepted: false, reason, citation и next action; ни один отрицательный case не вызывает внешний запрос. Положительный outcome допускает только тот scope, который совпал во всех четырёх доказательствах. Если эти условия нельзя проверить, готов не hand-off, а только следующий вопрос владельцу данных.
Симптом появляется в момент, когда инженер хочет ускорить разбор ошибки: он копирует в AI-инструмент фрагмент лога, тикет или stack trace, а потом не может точно восстановить, что стало контекстом запроса. Корпоративный тариф, разрешённый домен и устное «можно» отвечают на разные вопросы. Ни один из этих фактов сам по себе не доказывает допустимость передачи.
\nЦена ошибки измеряется не только возможной утечкой. В журнале могут остаться идентификатор клиента, редкое сочетание событий, токен, внутреннее имя сервиса или сведения, по которым запись легко сопоставить с человеком. После отправки нужно ещё установить, какой провайдер получил запрос, какие дополнительные данные добавила поверхность и какое решение владельца это разрешало. Поэтому решение принимаем до hand-off (передачи контекста), а при неполной проверке останавливаемся.
\nУдобно разложить передачу на четыре слоя. Первый слой — класс данных: что находится в записи и по какому правилу она получила метку public, internal или restricted. Класс принадлежит конкретной записи, а не папке и не ощущению автора. Маскирование имени не доказывает обезличивание: время, сумма, редкий идентификатор и последовательность событий могут сохранить связь с субъектом.
Второй слой — допустимость обработки. Здесь проверяют политику компании, договор с поставщиком и фактическую функцию: план, регион, режим, хранение, обучение моделей, subprocessors (привлечённых обработчиков) и журналирование, если они относятся к выбранной поверхности. Слово «корпоративный» — только название тарифа. Оно не заменяет чтение применимых условий.
\nТретий слой — egress, то есть технический выход запроса. Он включает destination (конечный адрес), прокси, API-шлюз, DNS и сетевые правила. Разрешённый firewall-маршрут доказывает лишь возможность соединения. NIST прямо отделяет защиту ресурса и проверку идентичности от доверия к сетевому расположению, поэтому доступный домен не превращается в разрешение на данные.
\nЧетвёртый слой — полномочия. Нужно связать решение с record id, requester (инициатором), ролью владельца, destination, целью и сроком действия. Старое одобрение «для логов» может относиться к другой записи, другой функции или уже истечь. Без этой связи нельзя доказать, что именно данный человек имел право отправить именно этот контекст именно в этот маршрут.
\nРассмотрим учебный запрос на объяснение ошибки в платёжном сервисе. В исходной записи есть время события, тип операции, идентификатор клиента и текст исключения. Инженер заменил идентификатор на client-17, но это лишь преобразование значения. Решение о классе должно учитывать весь остаточный набор полей и правило классификации.
Для проверки заведём карточку операции. Она не обязана содержать сам лог: достаточно ссылки на запись и метаданных, по которым другой инженер сможет повторить решение. Минимальный состав такой: record.id, версия классификации, класс, владелец, версия policy, выбранная функция, destination, сетевой control, requester, цель и expiresAt. Хэш или внутренний идентификатор payload полезен для связи событий, но не должен превращаться в новый журнал с исходными персональными данными.
| Слой | Доказательство | Проверяемый вопрос | Чего недостаточно |
|---|---|---|---|
| Класс данных | Метка, версия правила, владелец классификации | Что именно описывает запись и какие ограничения к ней применимы? | Удалённое имя или уверенность автора, что фрагмент «безопасный» |
| Policy и договор | Версия условий для плана, функции и региона | Допускает ли выбранная поверхность такую обработку? | Название тарифа или общая страница о продукте без нужного scope |
| Egress | Destination, маршрут и сетевой control | Куда уйдёт запрос и какие компоненты добавят данные? | Открытый домен, зелёный firewall или локальный прокси без трассировки |
| Полномочия | Record id, requester, owner decision и expiry | Кто и до какого момента разрешил эту операцию? | Устное согласие, общий доступ к проекту или старый approval |
Важно проверять не четыре флага по отдельности, а их пересечение. Разрешённый класс и подходящий договор не помогают, если запрос ушёл через BYOK-провайдера, которого не покрывает договор. Правильный маршрут и действующий доступ также не спасают restricted-запись, если правило классификации запрещает внешнюю обработку. В журнале решения полезно сохранить результат каждой проверки и причину отказа, но не копировать исходный payload.
\nУ пользователя перед глазами может быть одно поле prompt, а система соберёт больше. В официальном описании GitHub Copilot Chat указано, что prompt объединяется с дополнительным контекстом: открытыми файлами, репозиторием и историей чата; на некоторых поверхностях может добавляться веб-поиск. Значит, перед проверкой надо назвать не только текст, который человек собирается вставить, но и функцию, режим, открытые вкладки, индексированные источники и внешние инструменты.
\nЭто меняет порядок диагностики. Если инженер отправил stack trace из закрытого репозитория, нужно проверить не только строку с ошибкой, но и автоматически выбранные файлы. Если включён веб-поиск, отдельным destination становится поисковый API. Если используется BYOK, GitHub предупреждает, что prompt и ответы передаются выбранному провайдеру и могут подпадать под его правила хранения и приватности. На практике это означает: маршрут нельзя вывести из интерфейса; его нужно подтвердить документацией и конфигурацией конкретной поверхности.
\nКонтентные исключения и индексирование репозитория тоже имеют узкую роль. Настройка, запрещающая инструменту читать определённые файлы, уменьшает доступный контекст. Она не классифицирует остальные записи, не выдаёт пользователю право на отправку и не отменяет договорные ограничения. Так один control снижает конкретный путь утечки, но не закрывает четыре слоя целиком.
\nНиже — самостоятельный фрагмент JavaScript для Node.js без сети, файлов и вызова модели. Он проверяет контракт на фиксированных строках. В примере restricted не входит в список классов, разрешённых выбранной policy, поэтому результат должен быть отрицательным. Сохраните код в handoff-check.mjs и выполните node handoff-check.mjs.
const NOW = new Date('2025-04-15T10:00:00Z');\n\nfunction assess(request) {\n const checks = [\n {\n name: 'class-allowed',\n ok: request.service.allowedClasses.includes(request.record.class),\n },\n {\n name: 'policy-present',\n ok: Boolean(request.service.policyVersion),\n },\n {\n name: 'route-allowed',\n ok: request.route.allowlisted === true\n && request.route.destination === request.authorization.destination,\n },\n {\n name: 'authorization-current',\n ok: request.authorization.recordId === request.record.id\n && new Date(request.authorization.expiresAt) > NOW,\n },\n ];\n\n const failed = checks.filter((check) => !check.ok).map((check) => check.name);\n return {\n accepted: failed.length === 0,\n failed,\n nextAction: failed.length === 0 ? 'human-review' : 'ask-data-owner',\n payloadReleased: false,\n };\n}\n\nconst request = {\n record: { id: 'log-42', class: 'restricted' },\n service: { policyVersion: 'policy-2025-03', allowedClasses: ['public', 'internal'] },\n route: { destination: 'ai.example.test', allowlisted: true },\n authorization: {\n recordId: 'log-42',\n destination: 'ai.example.test',\n expiresAt: '2025-04-30T23:59:59Z',\n },\n};\n\nconsole.log(assess(request));\n// { accepted: false,\n// failed: [ 'class-allowed' ],\n// nextAction: 'ask-data-owner',\n// payloadReleased: false }\n\nОтрицательный результат здесь проверяем: запись не передаётся, а причина указывает на слой классификации. Домен ai.example.test зарезервирован для примеров и не вызывает соединение. Чтобы получить положительный результат, недостаточно удалить поле failed из вывода: нужно изменить класс на разрешённый и заново проверить все четыре условия. В рабочем коде добавьте строгую валидацию схемы, запрет неизвестных полей и отдельный тест для каждой причины отказа.
accepted: false, стабильную причину, ссылку на evidence и следующий вопрос владельцу. При полном совпадении передайте минимальный разрешённый контекст и сохраните событие без исходного текста.Журнал контроля должен помогать восстановить решение, но не становиться вторым каналом утечки. Для каждого запроса можно записать время, идентификатор операции, хэш или ссылку на запись, версии policy и классификации, выбранную поверхность, destination, результат четырёх проверок и причину остановки. Секреты, полный prompt и ответы модели в такой журнал не попадают. Срок хранения самого журнала выбирается по внутренней политике и договору, а не копируется из этого примера.
\nПроверка начинается с отрицательных случаев. Подайте тестовую запись класса restricted, просроченное разрешение, другой destination и неизвестную policy. У каждого запуска ожидайте accepted: false; сетевой мониторинг должен показать ноль внешних запросов. Затем отдельно проверьте разрешённый учебный класс и убедитесь, что передача доступна только при совпадении record id, маршрута и срока. Такой тест проверяет поведение шлюза, но не доказывает свойства поставщика после получения запроса.
Для периодического контроля сравнивайте фактический маршрут с карточкой операции: destination из журнала приложения, адрес прокси и включённые функции должны образовывать одну версию конфигурации. Расхождение — это отказ контроля, а не повод автоматически расширить allowlist. Сначала изолируйте операцию и назначьте владельца исправления, затем повторите проверку.
\nЧетыре слоя — инженерная модель принятия решения, а не универсальная сертификация. Она не заменяет DLP, инвентаризацию данных, контроль доступа, юридическую оценку, аудит поставщика или требования конкретной юрисдикции. NIST описывает рамки управления риском и zero trust, но не классифицирует ваш лог и не отвечает за условия вашего договора.
\nОфициальная документация продукта может измениться, а одна и та же функция может работать по-разному в GitHub.com, IDE, CLI и корпоративной конфигурации. Проверяйте дату, версию и surface, на которой действительно работает запрос. Даже доказанный маршрут не показывает, как модель интерпретирует контекст, и не гарантирует корректность ответа. Для критичного кода нужны human review, тесты и независимая проверка результата.
\nМаскирование снижает объём раскрываемых данных, но не делает любую запись публичной. Локальная модель уменьшает внешний egress, однако оставляет риски доступа, хранения, журналирования и компрометации хоста. Поэтому статья не отвечает «можно ли всегда отправлять логи». Она задаёт более узкий проверяемый критерий: можно ли доказать допустимость этой записи, этой функции, этого маршрута и этого полномочия в момент операции.
\nРешение готово к ограниченному применению, если другой инженер без исходного payload может взять карточку и воспроизвести четыре ответа: какой класс, какая policy и договорная версия, какой egress, кто разрешил и до какого срока. Для каждого отрицательного case известны код причины, evidence и следующий владелец вопроса. Ни один такой case не запускает внешний запрос. Если хотя бы один ответ строится на слове «обычно», интерфейсе без трассировки или старом согласовании, готов не hand-off, а новая проверка.
\nИнженер копирует в AI-инструмент десять строк лога, чтобы быстрее найти причину ошибки. В строках оказываются email, внутренний идентификатор и часть заголовка запроса. Интерфейс принимает текст. Ответ выглядит полезным. Через неделю никто не может точно сказать, какой контекст ушёл, к какому режиму сервиса он относился и кто разрешил передачу. Симптом проявился как удобная подсказка, а цена ошибки — потеря контроля над данными и дорогое расследование без исходного payload.
\nПроблема не решается одной настройкой «корпоративный тариф» или запретом на секреты в prompt. Передача должна пройти четыре независимые проверки: что находится во фрагменте, допускает ли policy и договор такую обработку, куда технически пойдёт запрос и кто разрешил именно этот объём данных. Если один вопрос подменяет остальные, команда получает ложное зелёное состояние.
\nКласс описывает содержимое. Например, public, internal или restricted. Он не говорит, можно ли передавать запись внешнему сервису. Это решает policy с учётом договора, режима хранения, обучения, региона и выбранного endpoint. Даже если policy разрешает класс, сеть должна вести запрос в нужное место, а сотрудник должен иметь право передать конкретный record.
Разделяйте эти факты в карточке контекста. Запишите идентификатор и версию записи, класс, основание допустимости, проверенное условие сервиса, символическое имя назначения, requester, владельца и срок разрешения. Не заполняйте неизвестное значение словом «внутреннее». Не переносите approval с соседнего файла. При неполном evidence путь закрывается.
\nПользователь видит поле prompt, но поставщик может обрабатывать его вместе с контекстом продукта. Это может быть открытый файл, история диалога, выбранный репозиторий или дополнительный поиск. Поэтому проверяйте не только видимый текст, но и конкретную поверхность, включённые функции и endpoint. Такой анализ относится к режиму продукта, а не ко всем AI-инструментам сразу.
\nСетевое правило отвечает на узкий вопрос: может ли трафик достичь назначения. Allow-list не классифицирует payload. DPA или другая договорённость описывает обязательства, но не доказывает, что firewall отправил запрос только в нужный endpoint. Разрешение владельца показывает полномочия, но не меняет класс записи и не продлевает истёкший срок. Каждый контроль должен иметь собственную проверку.
\nНиже приведён учебный JavaScript-пример. Он работает только с заранее заданными объектами в памяти. Он не вызывает модель, не читает файл, не отправляет лог и не утверждает ничего о production. Его задача — показать порядок проверок и отрицательный путь.
\nconst context = {\n id: 'example-17',\n version: 3,\n dataClass: 'synthetic-public',\n serviceCondition: 'synthetic-retention-reviewed',\n egressScope: 'fixed-ai-boundary-alpha',\n requester: 'alice',\n access: 'synthetic-read',\n authority: { owner: 'data-steward', scope: 'example-17', expires: '2026-12-31' }\n};\n\nfunction decide(record, policy, route, today) {\n const checks = [\n record.dataClass === policy.allowedClass,\n record.serviceCondition === policy.requiredCondition,\n record.egressScope === route.allowedScope,\n record.access === 'synthetic-read',\n record.authority.scope === record.id,\n record.authority.expires >= today\n ];\n\n return checks.every(Boolean)\n ? { status: 'review-ready', egressPerformed: false }\n : { status: 'stop', egressPerformed: false };\n}\n\nconst result = decide(\n context,\n { allowedClass: 'synthetic-public', requiredCondition: 'synthetic-retention-reviewed' },\n { allowedScope: 'fixed-ai-boundary-alpha' },\n '2026-08-02'\n);\nСтатус review-ready не означает, что запрос отправлен или что поставщик гарантирует нужный режим. Он означает только: фиксированная карточка прошла локальные проверки и может перейти к полномочному решению. Если изменить egressScope, срок или serviceCondition, функция возвращает stop. В реальной системе нужны собственные справочники, журнал решения и проверка фактического маршрута.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| В prompt попал production-лог | Класс записи не определён до копирования | Найти record и назначение класса у владельца данных | Остановить передачу, удалить локальную копию из рабочего контекста, запросить классификацию |
| Есть DPA, но неизвестен endpoint | Договор приняли за доказательство маршрута | Сверить plan, endpoint, proxy и сетевой журнал | Разрешать только явно названное назначение; иначе закрыть egress |
| Firewall пропускает запрос | Технический доступ приняли за допустимость payload | Сопоставить destination с policy и классом записи | Оставить сеть, но запретить payload до решения владельца |
| Approval есть в чате | Нет scope, срока или версии record | Проверить requester, record id, version, destination и expiry | Получить датированное разрешение; старое общее «можно» не переносить |
| Ответ модели содержит лишний контекст | Включён repository context, история или поиск | Проверить настройки конкретного режима и фактический запрос | Отключить дополнительный контекст или выбрать режим с проверенным scope |
Название инструмента, значок щита, корпоративная почта и доступ к приложению не доказывают допустимость конкретного payload. Слово «анонимизированный» тоже недостаточно: нужно знать, какие поля удалены, можно ли восстановить субъекта и кто проверил преобразование. Маскирование email не очищает токен в заголовке или идентификатор в URL.
\nНе смешивайте учебную карточку с реальной политикой. В примере используются значения с префиксом synthetic-; они не описывают вашу систему, vendor contract или фактическое хранение. Пример показывает форму fail-closed решения. Он не даёт production-результата и не заменяет legal, security или data-owner review.
Подход не отвечает за правильность самой классификации. Он делает неизвестность видимой. Если каталог данных устарел, approval выдан не тому владельцу или сетевой журнал неполон, проверка должна остановиться. Она также не доказывает, что ответ модели верен, что поставщик не изменит условия или что будущая функция инструмента сохранит прежний маршрут.
\nМатериал готов к применению в конкретной команде, когда для одного реального типа контекста можно предъявить четыре связанные записи: версионированный class, policy/contract condition для выбранного режима, проверенный egress и датированное scoped authorization. Отрицательный тест должен показать stop при истёкшем сроке, чужом scope или неизвестном endpoint. После этого команда может передавать только разрешённый минимальный фрагмент и восстановить, почему решение было принято.
\nИнженер открывает AI-чат, чтобы разобраться с ошибкой, и вставляет десять строк production-лога. В строках остаются email, внутренний идентификатор и фрагмент заголовка запроса. Ответ выглядит полезным, но по нему нельзя понять, какие ещё данные добавила выбранная поверхность, какой endpoint получил запрос и имел ли сотрудник право передавать именно эту запись.
\nЗдесь полезно разделить четыре независимых вопроса: что находится во фрагменте, разрешает ли policy выбранную обработку, куда технически уйдёт запрос и кто выдал полномочия на конкретный scope. «Корпоративный» аккаунт, allow-list в firewall или согласие в чате отвечает только на часть вопросов. Если любой из них не доказан, безопасный результат — остановить отправку и оставить причину для следующей проверки.
\nДо открытия внешнего интерфейса зафиксируйте объект передачи. У него должны быть recordId, версия, источник, владелец данных и назначенный класс. Класс описывает содержимое, а не право отправить его наружу: public, internal и restricted — это свойства данных, но не готовое разрешение для любого сервиса.
Минимальная карточка должна связывать данные с решением. В ней укажите цель, requester (инициатора), назначение, версию policy, условие хранения, срок действия и ссылку на полномочие владельца. Не подменяйте неизвестный класс словом «внутренний» и не переносите разрешение с соседнего файла: другой record, режим или endpoint создаёт новое решение.
\nПоле чата показывает только часть контекста. Конкретный продукт может добавить историю диалога, имя репозитория, открытый файл, результаты поиска, выбранные расширения или служебные параметры. Это не универсальное свойство всех AI-инструментов: состав нужно проверять для конкретной поверхности, тарифного режима и включённых функций.
\nДокументация GitHub для Copilot Chat в GitHub прямо описывает обработку пользовательского ввода вместе с контекстом репозитория и, в некоторых случаях, результатами Bing. В той же документации указано, что ответы нужно проверять, а для чувствительного кода — просматривать и тестировать результат. Поэтому в карточке фиксируйте не только текст вопроса, но и surface, включённые источники контекста и фактическое назначение.
\nПосле формирования запроса нужен второй барьер: prompt builder не должен принимать данные, пока детерминированная проверка не вернула разрешённый исход. Это не оценка качества ответа модели. Её результат может быть неверным даже при полностью разрешённом payload, поэтому проверка безопасности и проверка корректности ответа остаются разными этапами.
\nПервая проверка отвечает за содержимое: классификатор или владелец данных подтверждает, что record относится к заявленному классу после маскирования. Вторая отвечает за условия обработки: policy и договор должны относиться к выбранному продукту, режиму, региону и сроку хранения. Слово «анонимизированный» само по себе не заменяет проверку остаточных идентификаторов и возможности восстановления субъекта.
\nТретья проверка отвечает за маршрут. Allow-list говорит, что соединение разрешено к определённому назначению, но не делает любой payload допустимым. Укажите endpoint, прокси, включённый поиск и дополнительные интеграции. Если правило разрешает домен шире, чем договорённый сервис, это повод сузить маршрут или остановить отправку.
\nЧетвёртая проверка отвечает за полномочия. Requester должен иметь доступ к record, а владелец должен разрешить цель, объём, назначение и срок. Авторизация без recordId и expiry не даёт воспроизводимого scope. Принцип zero trust NIST формулирует похожую границу: доверие не следует выводить только из сетевого положения или принадлежности устройства, а аутентификацию и авторизацию нужно выполнять для конкретного ресурса.
| Контроль | Что доказывает | Чего не доказывает | Действие при пробеле |
|---|---|---|---|
| Класс record | Содержимое отнесено к правилу с владельцем и версией | Что внешний сервис вправе его обрабатывать | Остановить и запросить классификацию |
| Policy и условия сервиса | Для выбранной surface описаны обработка, хранение и режим | Что запрос пошёл именно по этому маршруту | Проверить конкретный план, endpoint и дату действия |
| Egress allow-list | Сеть допускает заявленное техническое назначение | Что payload соответствует policy и полномочиям | Не отправлять данные до сверки с классом и scope |
| Scoped authorization | Конкретный requester получил срок и цель для record | Что сервис не добавит другой контекст | Получить новое разрешение и проверить surface |
| Request trace | Виден фактический outbound payload и destination | Что ответ модели точен или безопасен для публикации | Отделить расследование передачи от ревью ответа |
Ниже — самостоятельный пример для Node.js 20 и новее. Он работает только с объектами в памяти, не читает файлы, не вызывает модель и не делает HTTP-запрос. Входы с префиксом synthetic- намеренно учебные. Команда запуска показывает отрицательный путь: restricted-класс блокирует hand-off даже при наличии пользователя и маршрута.
const request = {\n recordId: 'synthetic-log-v1',\n dataClass: 'restricted',\n serviceCondition: 'synthetic-reviewed-retention',\n destination: 'synthetic-ai-boundary-alpha',\n requester: 'synthetic-engineering-read',\n authorization: {\n scope: 'synthetic-log-v1',\n purpose: 'synthetic-debugging',\n expiresAt: '2026-12-31T00:00:00Z'\n }\n};\n\nconst policy = {\n allowedClasses: ['synthetic-public'],\n serviceCondition: 'synthetic-reviewed-retention',\n destination: 'synthetic-ai-boundary-alpha',\n purpose: 'synthetic-debugging'\n};\n\nfunction decide(input, rules, now) {\n const checks = {\n classAllowed: rules.allowedClasses.includes(input.dataClass),\n serviceReviewed: input.serviceCondition === rules.serviceCondition,\n routeAllowed: input.destination === rules.destination,\n scopeMatches: input.authorization.scope === input.recordId,\n purposeMatches: input.authorization.purpose === rules.purpose,\n authorizationActive: input.authorization.expiresAt > now.toISOString()\n };\n\n const allowed = Object.values(checks).every(Boolean);\n return {\n decision: allowed ? 'hand-off' : 'stop',\n reason: allowed ? null : Object.entries(checks).filter(([, ok]) => !ok).map(([name]) => name),\n egressPerformed: false\n };\n}\n\nconst result = decide(request, policy, new Date('2026-08-02T12:00:00Z'));\nconsole.log(JSON.stringify(result, null, 2));\nЗапустите сохранённый фрагмент командой node preflight.mjs. Ожидаемый результат содержит \"decision\": \"stop\", причину classAllowed и \"egressPerformed\": false. Важно, что функция не строит prompt и не передаёт его HTTP-клиенту. В реальном проекте замените учебные справочники на версионированную policy, но сохраните такой же отрицательный контракт.
Положительный результат тоже ограничен. Если заменить класс на synthetic-public, функция проверит только перечисленные поля. Она не доказывает реальное поведение поставщика, полноту сетевого журнала, юридическое основание обработки или отсутствие скрытого контекста в интерфейсе. Это gate перед следующим полномочным шагом, а не сертификат безопасности.
Сначала остановите повторение: отключите интеграцию или запретите дальнейший egress для этой surface. Не удаляйте единственную копию доказательств до согласования с владельцем расследования. Сохраните минимум, необходимый для анализа: время, requester, идентификатор операции, destination, режим продукта и классификацию без копирования чувствительного payload в новый тикет.
\nЗатем разделите два вопроса. Факт передачи устанавливается по журналам клиента, прокси и поставщика, если они доступны. Состав исходного payload устанавливается по журналу формирования запроса, а не по ответу модели. Если одного из журналов нет, это ограничение результата расследования, а не основание считать, что утечки не было.
\nrecordId, версию, источник, владельца и цель. В заявке используйте ссылку или безопасный отпечаток, а не необработанный лог.Этот маршрут не классифицирует данные автоматически и не заменяет DLP, privacy review, договор с поставщиком, управление доступом или требования закона. NIST AI RMF — добровольная рамка для управления рисками AI; она помогает организовать функции govern, map, measure и manage, но не выдаёт разрешение на конкретную запись. Решение о законности обработки остаётся у вашей организации и зависит от юрисдикции и контекста.
\nЛокальная модель уменьшает внешний маршрут, но не отменяет права доступа, локальное хранение журналов и риск попадания секрета в историю. Маскирование уменьшает объём, но может оставить комбинацию полей, по которой субъект восстанавливается. Человеческое согласование полезно для спорных случаев, но оно не должно быть единственным барьером, если запросы проходят через автоматическую интеграцию.
\nПроверка также не говорит, что ответ модели верен. Для кода нужны ревью, тесты и сканирование зависимостей; для инцидента — независимое подтверждение фактов; для решения о клиенте — отдельная процедура. Без этих этапов безопасный hand-off всё равно может привести к неверному действию.
\nПуть можно включать для конкретного типа данных, если другой инженер воспроизведёт его без устного контекста. Должны существовать версия классификации, ссылка на policy для конкретной surface, запись проверенного destination, scoped authorization и журнал решения. Негативный тест должен показывать stop при restricted-классе, чужом scope, истёкшем сроке и неизвестном endpoint.
\nЕсли хотя бы одно доказательство отсутствует, не называйте результат «разрешённым». Оставьте данные локально, верните причину отказа и назначьте владельца следующего вопроса. После появления недостающего evidence повторите весь pre-flight: изменение режима, маршрута или версии record делает старое решение неприменимым.
\nПоиск по инженерной базе часто ломается не тогда, когда ничего не нашёл. Опаснее другой симптом: система возвращает убедительный фрагмент, а команда принимает его за действующее правило. Документ уже закрыт для текущего пользователя, срок его действия истёк или в нём нет точного места, которое подтверждает вывод. Поиск показывает высокий score, интерфейс показывает ответ, а ошибка обнаруживается позже.
\nЦена такой ошибки измеряется не длиной задержки. Старая инструкция может привести к неверной миграции. Закрытый фрагмент может попасть в ответ человеку без права доступа. Неточная цитата превращает предположение в решение на ревью. Исправлять последствия дороже, чем остановить ответ на несколько минут и передать вопрос владельцу источника.
\nRetrieval должен отвечать на два разных вопроса. Первый: насколько найденный фрагмент похож на запрос. Второй: можно ли использовать его для конкретного ответа. Векторный или текстовый поиск решает только первый вопрос. Он ранжирует результаты. Он не подтверждает дату, права и точный смысл документа.
\nПоэтому ответ разрешается продолжить только для записи, которая одновременно проходит три проверки: источник доступен этому запросу, источник свеж по заданному правилу и в нём есть точная цитата с устойчивым anchor. Если хотя бы одно условие не выполнено у всех кандидатов, система возвращает stop. Она не заполняет пробел вероятным пересказом.
\nStop не означает «в базе ничего нет». Он означает «найденное нельзя безопасно использовать». Это важное различие для диагностики. Пользователь должен увидеть безопасную причину и следующий маршрут, но не закрытый текст. Владелец документа должен понять, что обновить или разрешить. Так система сохраняет и полезность, и границу доказательств.
\nУ записи источника должен быть контракт. Минимальный набор полей выглядит так: стабильный идентификатор, версия, URI, anchor, владелец, класс доступа, дата публикации, дата индексации и локальная дата истечения. Поле retrievedAt фиксирует момент, когда поиск увидел запись. Эти даты нельзя сводить к одному timestamp: задержка индекса и срок действия документа описывают разные риски.
Сначала система получает кандидатов. Затем применяет policy-фильтр. Проверка прав происходит до передачи excerpt в контекст ответа. Проверка свежести зависит от типа вопроса. Для операционного вопроса истёкшая инструкция непригодна. Для исторического вопроса она может быть полезна, но ответ должен назвать версию и дату. В обоих случаях правило задают метаданные и владелец политики, а не score.
\nПоложительная ветка возвращает source id, version, URI, anchor, retrievedAt и разрешённый фрагмент. Отрицательная ветка возвращает код причины: access-denied, expired, missing-anchor или unknown-policy. Закрытый excerpt не входит в отрицательный результат. Такой контракт не делает источник истинным автоматически. Он не даёт системе скрыть отсутствие доказательства.
const result = retrieve(query, records, policy);\n\nif (!result.accepted) {\n return {\n status: 'stop',\n code: result.reason,\n sourceId: result.sourceId,\n next: result.ownerAction,\n };\n}\n\nreturn {\n status: 'review',\n claim: draftClaim(query, result.excerpt),\n citation: {\n uri: result.uri,\n version: result.version,\n anchor: result.anchor,\n retrievedAt: result.retrievedAt,\n },\n};\nЭто учебный пример. Функции retrieve и draftClaim здесь не подключены к настоящей базе и не подтверждают реальную авторизацию. Пример показывает границу: положительный результат ещё требует ручного сравнения утверждения с источником, а отрицательный результат останавливает дальнейшую обработку.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Высокий score, но документ старый | Ранжирование не учитывает срок действия | Сравнить expiresAt и retrievedAt | Остановить ответ и направить к владельцу документа |
| Найден закрытый фрагмент | Поиск и проверка прав разделены или проверка выполнена поздно | Проверить access decision до передачи excerpt | Вернуть безопасную причину без текста источника |
| Цитата ведёт на страницу без места | В индексе сохранён URI, но нет устойчивого anchor | Открыть URI и проверить заголовок, номер раздела или якорь | Остановить ответ и исправить карточку источника |
| Разные даты в индексе и документе | Не различены публикация, индексация и срок действия | Сопоставить все даты и владельца каждой | Исправить policy или обновить индекс |
| Система отвечает «ничего нет» | Reject reasons потерялись после фильтра | Проверить structured result и журнал решения | Показать safe reason и следующий маршрут |
HTTP-заголовок Last-Modified сообщает дату, когда origin считает выбранное представление изменённым. Это полезный сигнал для индексации и условных запросов. Но он не доказывает, что инструкция всё ещё действует. Правило могло устареть из-за миграции, смены владельца или изменения контекста, даже если файл с тех пор не менялся.
Практическая политика должна назвать, какая дата управляет ответом. Например, publishedAt отвечает за версию документа, indexedAt показывает задержку конвейера, а expiresAt задаёт допустимый срок для операционных вопросов. Если источник не предоставляет нужные данные, это не повод угадывать. Нужна остановка или явное решение владельца о том, какое ограничение действует.
Доступ к документу нельзя выводить из факта, что поисковый сервис смог его прочитать. Индекс может работать с расширенными правами, а запрос пользователя — с ограниченными. Проверка должна учитывать субъекта, ресурс и цель обращения. Deny должен произойти до того, как закрытый фрагмент попадёт в контекст следующего шага.
\nБезопасный stop-ответ содержит только то, что разрешено policy: например, внутренний идентификатор записи, общий код причины и группу владельца. Не следует возвращать закрытый заголовок, соседние предложения или признаки, по которым можно восстановить содержание. Уровень подробности диагностического сообщения — тоже часть модели доступа.
\nКачество retrieval видно не только по найденным документам. Оно видно по тому, как система ведёт себя при конфликте. Если документ похож на запрос, но истёк, она должна сохранить причину и остановиться. Если фрагмент разрешён, но anchor отсутствует, она не должна ссылаться на всю страницу. Если policy неизвестна, она не должна превращать неизвестность в allow.
\nПростой тест проверяет именно это поведение: highest-score candidate получает значение expired или access-denied; итоговый объект имеет статус stop; поля claim и citation.excerpt отсутствуют; присутствуют причина и действие владельца. Это проверяет контракт учебного модуля, но не доказывает работу вашей production-базы. Для реальной системы нужны отдельные проверки identity, источника и наблюдаемости.
Эта модель не выбирает лучший алгоритм поиска и не обещает, что keyword search, embeddings или reranking дадут конкретную точность. Она не заменяет классификацию документов, аудит прав и управление версиями. Score остаётся полезным для порядка кандидатов, но не становится доказательством. На качество ответа также влияет полнота корпуса: если нужного документа нет, фильтр не создаст его.
\nУчебный код не читает настоящую wiki, не обращается к сети и не выполняет реальную authorization decision. Все записи в примере условны. Нельзя переносить его значения дат, score или статусы в production. Переносить стоит только форму результата: accepted либо stop, структурированную причину, citation с anchor и явного владельца следующего действия.
\nРабота готова, когда для одного выбранного типа вопроса система может показать полный путь. Разрешённый свежий источник даёт версию, URI, anchor и дату retrieval, после чего человек подтверждает claim. Просроченный, закрытый или нецитируемый кандидат даёт stop без раскрытия запрещённого текста. В обоих случаях есть воспроизводимая проверка и назначенный владелец.
\nЕсли тест с высоким score и истёкшим документом всё ещё формирует ответ, проблема находится не в формулировке prompt. Не хватает контракта между индексом, policy и слоем ответа. Сначала добавьте metadata и отрицательную ветку. Только после этого имеет смысл настраивать ranking.
\nПредставим рабочий вопрос: «Как откатить релиз?» Поиск возвращает абзац из runbook с высоким score. В нём есть знакомые слова, но документ уже заменён, фрагмент закрыт для текущей роли или ссылка ведёт только на главную страницу. Если сразу передать текст в ответ, система превратит совпадение слов в указание к действию.
\nЗдесь опасен не сам поиск. Ошибка возникает на границе между найденным кандидатом и разрешённым утверждением. Старый runbook может привести к неправильной последовательности отката. Закрытый фрагмент нельзя раскрывать даже ради «полезного контекста». Ссылка без точного места не позволяет коллеге перепроверить вывод. Поэтому зрелый retrieval должен уметь завершаться ответом stop, когда доказательств недостаточно.
Retrieval — этап, который выбирает фрагменты из корпуса по запросу. Текстовый индекс, embeddings и reranker помогают упорядочить кандидатов. Их score отвечает на вопрос «насколько запись похожа на запрос по данной модели». Он не отвечает на вопросы «действует ли правило», «можно ли его показать этой роли» и «есть ли в источнике точное место для цитирования».
\nЭти вопросы нужно разделить в контракте. Пусть кандидат проходит три независимые проверки: доступ разрешён субъекту запроса, срок пригодности не истёк по политике конкретного типа вопроса, а источник содержит устойчивый anchor — заголовок, номер раздела или другой способ указать точное место. Только после этого разрешённый фрагмент можно передавать в слой формирования ответа.
\nЕсли лучший по score кандидат не прошёл проверку, это ещё не означает немедленный stop: можно проверить следующий кандидат. Но если все подходящие записи отклонены, система обязана вернуть безопасную причину и маршрут к владельцу. Она не должна «додумывать» недостающий фрагмент из похожих документов.
\nПоиск не сможет проверить то, чего нет в метаданных. Для каждой записи полезно хранить стабильный sourceId, version, канонический uri, anchor, класс доступа, владельца, publishedAt, indexedAt и управляемый политикой expiresAt. Это не универсальная схема базы данных, а минимальный набор для рассматриваемого решения. Поля надо согласовать с владельцем корпуса и правилами доступа.
Даты имеют разные смыслы. publishedAt описывает версию, indexedAt показывает задержку индексатора, а expiresAt — локальное правило, после которого операционный вопрос нельзя закрывать этой записью без подтверждения. Для исторического вопроса срок может быть другим: старый документ допустим как описание прошлого, но ответ должен назвать версию и дату.
Не подменяйте это правило заголовком HTTP. RFC 9110 определяет Last-Modified как время, когда origin, по собственному мнению, в последний раз изменил выбранное представление. Заголовок полезен для условных запросов и кэша, но не говорит, отменено ли содержание документа, изменился ли владелец или разрешён ли доступ. Семантический срок действия остаётся частью политики корпуса.
Индексатор может читать больше, чем конкретный пользователь. Поэтому факт, что поисковый сервис нашёл запись, ничего не доказывает о праве показать её субъекту запроса. Сначала определите субъекта, ресурс и цель обращения, затем получите решение authorization. Только после положительного решения можно раскрывать excerpt.
\nБезопасный stop-ответ не пересказывает запрещённый текст. В нём достаточно общего кода access-denied, expired, missing-anchor или policy-unknown и следующего действия: запросить доступ, назначить владельца, обновить карточку или выбрать другой корпус. Даже название закрытого документа может быть чувствительным, поэтому его возврат тоже должен проходить через policy.
Это согласуется с моделью Zero Trust из NIST SP 800-207: доверие не выводится из нахождения в локальной сети или из принадлежности ресурса организации, а authentication и authorization рассматриваются как разные функции до установления сессии к ресурсу. Стандарт не описывает ваш индекс и не выдаёт готовую ACL; он задаёт границу, которую нельзя заменять сетевой близостью.
\nНиже — самостоятельный пример для Node.js 18+ без внешних пакетов. Данные синтетические, дата намеренно зафиксирована, чтобы результат не менялся завтра. Сохраните фрагмент как retrieval-check.mjs или вставьте его в оболочку node --input-type=module. Ожидаемый вывод: сначала разрешён текущий документ с меньшим score, затем stop после искусственного истечения всех записей.
const records = [\n {\n sourceId: 'runbook-legacy',\n version: '4',\n score: 0.99,\n accessClass: 'internal',\n expiresAt: '2025-02-01',\n uri: 'https://docs.example.test/rollback',\n anchor: 'rollback-release'\n },\n {\n sourceId: 'runbook-private',\n version: '7',\n score: 0.98,\n accessClass: 'restricted',\n expiresAt: '2025-08-01',\n uri: 'https://docs.example.test/rollback',\n anchor: 'rollback-release'\n },\n {\n sourceId: 'runbook-current',\n version: '8',\n score: 0.75,\n accessClass: 'internal',\n expiresAt: '2025-08-01',\n uri: 'https://docs.example.test/rollback',\n anchor: 'rollback-release'\n }\n];\n\nconst policy = {\n asOf: '2025-03-20',\n allowedClasses: new Set(['internal'])\n};\n\nfunction checkCandidate(record, currentPolicy) {\n if (!currentPolicy.allowedClasses.has(record.accessClass)) {\n return { accepted: false, reason: 'access-denied' };\n }\n if (!record.expiresAt || record.expiresAt < currentPolicy.asOf) {\n return { accepted: false, reason: 'expired' };\n }\n if (!record.uri || !record.anchor) {\n return { accepted: false, reason: 'missing-anchor' };\n }\n return { accepted: true };\n}\n\nfunction retrieve(candidates, currentPolicy) {\n const reasons = [];\n for (const record of [...candidates].sort((a, b) => b.score - a.score)) {\n const decision = checkCandidate(record, currentPolicy);\n if (decision.accepted) {\n return {\n status: 'accepted',\n sourceId: record.sourceId,\n version: record.version,\n citation: record.uri + '#' + record.anchor\n };\n }\n reasons.push(decision.reason);\n }\n return { status: 'stop', reasons: [...new Set(reasons)], next: 'owner-review' };\n}\n\nconsole.log(retrieve(records, policy));\nconsole.log(retrieve(records.map((record) => ({ ...record, expiresAt: '2025-01-01' })), policy));\nПервая строка выбирает runbook-current: два более похожих кандидата отклонены по разным причинам, а доступный свежий документ имеет точный anchor. Вторая возвращает status: 'stop' и причины без excerpt. Это проверяет форму контракта, но не настоящую авторизацию: allowedClasses здесь передаётся вручную и не является решением доверенного сервиса.
| Симптом | Что он доказывает | Что проверить | Следующий шаг |
|---|---|---|---|
| Высокий score у старого документа | Кандидат похож на запрос по модели ранжирования | expiresAt, тип вопроса, владелец политики | Проверить следующий кандидат или вернуть expired |
| Индекс возвращает закрытый excerpt | Индексатор видит запись | Решение доступа до формирования контекста | Убрать текст из результата и вернуть безопасный код |
| URI открывается, но место не найдено | Ссылка существует | Версию и устойчивый заголовок или fragment | Остановить цитирование и исправить карточку |
| Документ не менялся год | Представление долго не изменялось по данным origin | Отменённые правила, владельца и семантический срок | Не считать Last-Modified доказательством актуальности |
| Ответ говорит «ничего нет» | После фильтров не осталось разрешённого кандидата | Сохранённые reject-коды и границу раскрытия | Показать причину и адресата, не скрывая следующий шаг |
Stop полезен только тогда, когда его можно отличить от пустого индекса и от сбоя самого retrieval. Внутри системы сохраняйте request id, тип запроса, количество кандидатов, причины отклонения и версию policy. Отдельно измеряйте долю остановок по причинам. Не записывайте в диагностический журнал закрытый excerpt или чувствительные названия без разрешения.
\nРазделяйте три результата: no-match — подходящих кандидатов не найдено; stop — кандидаты были, но ни один нельзя использовать; system-error — проверка не завершилась из-за сбоя. Пользовательский интерфейс может показывать один безопасный текст, но внутренний контракт должен сохранять различие. Иначе команда начнёт чинить ranking, когда сломана policy, или повторять запрос при отказе авторизации.
Проверяйте и положительную ветку вручную. Откройте exact source, сравните формулировку утверждения с указанным anchor, убедитесь в версии и проверьте, что право относится к тому же субъекту и ресурсу. Высокий score, зелёный health-check или наличие ссылки не заменяют эту сверку.
\nexcerpt и citation в stop-ответе.Проверка доступа, срока и anchor не делает документ истинным. Она не оценивает полноту корпуса, качество формулировки, причинность рекомендации или пригодность бизнес-решения. Утверждение может быть точно процитировано и всё равно оказаться неверным из-за ошибки самого источника. Поэтому для рискованных операций нужен отдельный review владельца процесса.
\nScore нельзя сравнивать между разными индексами и моделями без отдельной калибровки. Значение 0.99 в учебном примере не является универсальным порогом. Текстовый поиск, embeddings и reranking имеют разные свойства; выбор алгоритма остаётся инженерным экспериментом на вашем корпусе и eval-наборе.
Локальное поле expiresAt не является стандартным HTTP-заголовком и не появляется автоматически из Last-Modified. Срок должен поддерживаться процессом владельца. NIST SP 800-207 не заменяет вашу матрицу ролей, а RFC 9110 не определяет жизненный цикл wiki-документа. Эти границы нужно оставить видимыми, иначе учебный контракт начнут выдавать за готовую платформу.
Маршрут готов к использованию, если независимый проверяющий может восстановить запрос, policy и выбранную версию источника. Для свежей разрешённой записи ответ содержит точный URI и anchor, а утверждение совпадает с открытым фрагментом. Для просроченной, запрещённой или нецитируемой записи результатом становится stop без раскрытия текста и с понятным следующим действием.
Начинайте с одного типа операционных вопросов и пяти негативных fixtures из списка выше. Если система не проходит этот малый набор, добавление ещё одного reranker только увеличит число убедительных, но непроверяемых ответов. Правильный stop — не провал поиска, а честная граница его доказательной силы.
\nLast-Modified и описывает его применение как валидатора представления. RFC не задаёт срок действия бизнес-документа.