From 9e8d02f320cbfad96a25a9898c4c33d56ea4edb0 Mon Sep 17 00:00:00 2001 From: "E.Gavrilov" Date: Thu, 3 Sep 2026 15:16:54 +0300 Subject: [PATCH] editorial: revise articles 095-100 to 10/10 --- editorial/agent-rewrites/095.json | 4 ++-- editorial/agent-rewrites/096.json | 6 +++--- editorial/agent-rewrites/097.json | 6 +++--- editorial/agent-rewrites/098.json | 6 +++--- editorial/agent-rewrites/099.json | 4 ++-- editorial/agent-rewrites/100.json | 4 ++-- 6 files changed, 15 insertions(+), 15 deletions(-) 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

Механизм: пять артефактов, пять вопросов

\n

У каждой фазы свой владелец и свой вопрос. Preview отвечает, что планировщик рассчитал в фиксированный момент. Authority отвечает, имеет ли операция право работать с выбранным scope. Approval отвечает, согласовал ли reviewer именно эту операцию. Audit trail отвечает, какое событие записал исполнитель. Verification отвечает, видит ли авторитетный reader ожидаемый результат.

\n

Ни один ответ не следует из другого. Наличие approval не доказывает, что reviewer видел полный список целей. Запись execution не доказывает, что target system приняла каждое изменение. Успешный dry-run не доказывает, что следующий write применится к тому же состоянию.

\n
Что на самом деле доказывает каждый артефакт
АртефактЧестное утверждениеОпасная подменаСледующая проверка
PreviewПланировщик рассчитал proposed change для выбранного snapshotExecute будет ровно таким жеПересчитать preconditions перед write
AuthorityPolicy разрешает selector и размер scopeChange полезен и технически корректенПроверить intent и содержимое preview
ApprovalReviewer разрешил связанную карточкуReviewer проверил каждую цель и побочный эффектСверить operation id и digests
Audit trailИсполнитель записал событие по контрактуTarget system уже находится в ожидаемом состоянииСделать независимый read
VerificationReader увидел заданное условиеВсе последствия change безопасныПроверить остаточный риск и recovery
\n

Preview имеет срок годности

\n

Preview — снимок намерения, а не разрешение на исполнение. Между расчётом и write другой job может изменить объект. Policy может обновиться. Selector может начать выбирать другой набор. Поэтому в карточке хранят не только человекочитаемый diff, но и operation id, snapshot version, selector, dense target list, target digest, expected precondition и proposal digest.

\n

Terraform разделяет speculative plan и план, который можно применить. Официальная документация прямо предупреждает: изменения в целевой системе после раннего speculative plan могут поменять финальный эффект, поэтому перед apply нужно проверить актуальный non-speculative plan. Этот принцип переносится на самописный runner без привязки к Terraform: старый diff нужно пересчитать или отклонить по TTL.

\n

Dry-run тоже бывает разным. В Kubernetes kubectl apply --dry-run=client только печатает объект и не отправляет его. Режим --dry-run=server отправляет server-side request, но не сохраняет ресурс. Первый проверяет локальную сериализацию. Второй проходит часть серверной обработки. Ни один режим не подтверждает будущий persistent change.

\n

Authority ограничивает мощность

\n

Authority не решает, стоит ли менять поле. Она ограничивает то, что исполнитель может сделать: допустимый selector, максимальное число целей, срок действия и исключения. Если policy разрешает два объекта, карточка с тремя не должна превращаться в warning. Runner возвращает stop до approval.

\n

Граница должна быть машинной. Сравнивайте exact selector и exact target digest. Не полагайтесь на текст «небольшой batch». Не уменьшайте список молча: reviewer должен увидеть тот же scope, который получит executor. Не расширяйте authority по умолчанию, если часть целей стала недоступна.

\n

Учебный пример: остановка до write

\n

Ниже — самостоятельная модель в памяти. Она не обращается к сети, не читает права и не меняет production. Карточка содержит три synthetic targets, а authority разрешает два. Отрицательный путь важнее счастливого: операция не доходит до approval.

\n
const operation = {\n  id: 'op-normalize-labels-v1',\n  selector: 'labels.env=staging',\n  targets: ['service-a', 'service-b', 'service-c'],\n  targetDigest: 'targets-a-b-c-v1',\n  proposalDigest: 'proposal-7f2a'\n};\n\nconst authority = {\n  id: 'authority-l2-v1',\n  selector: 'labels.env=staging',\n  maxTargets: 2\n};\n\nfunction requestApproval(op, policy) {\n  if (op.selector !== policy.selector) {\n    return { status: 'STOP', reason: 'selector-not-authorized' };\n  }\n  if (op.targets.length > policy.maxTargets) {\n    return {\n      status: 'STOP',\n      reason: 'scope-exceeds-authority-limit',\n      targetCount: op.targets.length\n    };\n  }\n  return { status: 'READY_FOR_APPROVAL', operationId: op.id };\n}\n\nconsole.log(requestApproval(operation, authority));\n// { status: 'STOP', reason: 'scope-exceeds-authority-limit', targetCount: 3 }
\n

Пример проверяет контракт, а не безопасность пользователя. В реальной системе policy получает identity из провайдера, срок из доверенных часов, а target list — из авторитетного inventory. Здесь значения фиксированы, чтобы показать один переход: превышение лимита останавливает цепочку до любого внешнего действия.

\n

Approval должен быть связанным

\n

Approval без 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.

\n

Audit trail не наблюдает состояние

\n

Журнал связывает фазы одной операции. В нём полезны operation id, version runner, selector, digests, authority, approval, timestamps и результат каждого target. Это помогает восстановить причинную цепочку. Но журнал фиксирует сообщение исполнительной системы. Он не читает целевой объект и не доказывает, что тот сохранился.

\n

Разделяйте receipt и evidence. Receipt говорит: «executor отправил запрос и получил ответ». Evidence говорит: «authoritative reader прочитал поле после операции». Если dashboard помечает change как verified только по наличию audit event, он скрывает самый дорогой отказ — неизвестное состояние.

\n

Verification и rollback

\n

Verification начинается с явного expected state. Например: reader видит у двух объектов label env=staging, target digest совпадает с карточкой, отсутствующие объекты перечислены, а stale inventory имеет отдельный статус. Exit code исполнителя не заменяет этот read.

\n

Если reader недоступен, состояние неизвестно. Если один объект не совпал, операция остановлена. Не переводите эти исходы в «успех с предупреждением», если следующий запуск может затронуть тот же scope.

\n

Rollback не является обратной строкой в finally. После частичного выполнения исходное значение может быть устаревшим. Часть объектов может исправить человек. Новая policy может запретить обратную операцию. Безопасный rollback начинается с observed state, нового bounded scope, новой authority, нового preview и нового approval. Исходное approval не наследуется.

\n

Симптом → причина → проверка → действие

\n
Диагностика автоматизированного change
СимптомПричинаПроверкаДействие
После 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
\n

Иллюстрация границ

\n
\"Матрица
Authority ограничивает scope, но не оценивает смысл идеи. Approval связывает карточку. Verification проверяет состояние после execute. Красная клетка означает STOP, а не автоматический rollback.
\n

Порядок действий

\n
  1. Опишите операцию. Запишите intent, selector, target list, target digest, expected before и expected after.
  2. Зафиксируйте границу preview. Укажите snapshot version, источник данных, время расчёта и условие устаревания.
  3. Проверьте authority. Сравните selector, лимит, срок и исключения до запроса approval.
  4. Свяжите approval. Сохраните operation id, proposal digest, target digest и authority id. Любое несовпадение остановите.
  5. Повторите preconditions перед write. Проверьте существование целей, версию, scope и действительность policy.
  6. Запишите receipt отдельно. Не называйте событие исполнителя доказательством состояния.
  7. Сделайте независимую verification. Прочитайте authoritative source и сравните его с expected state.
  8. Остановите неизвестное. При mismatch или недоступном reader соберите observed inventory и спланируйте новый bounded change. Не запускайте широкий rollback автоматически.
\n

Ограничения

\n

Эта схема не создаёт identity provider, неизменяемое хранилище журнала, криптографическую подпись, распределенный lock или гарантию идемпотентности. Учебный код не читает реальные targets и не даёт production-результатов. Digests в примере — строки для проверки связи, а не доказательство криптографической стойкости.

\n

Независимая verification тоже имеет границу. Reader может быть устаревшим, неполным или не видеть побочные системы. Назначьте источник истины и отдельно опишите, что означает unknown. Для destructive change нужны дополнительные ограничения: backup, ручное подтверждение, лимит скорости и владелец recovery.

\n

Approval не делает решение правильным. Он только фиксирует разрешение в определённой роли. Authority не проверяет бизнес-смысл. Audit trail не гарантирует сохранность без свойств storage. Эти ограничения нужно оставить рядом с контрактом, иначе короткий статус начнёт обещать больше, чем проверка.

\n

Проверяемый критерий готовности

\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

Проверяемые источники

" + "excerpt": "Preview показывает рассчитанное намерение, authority ограничивает допустимый scope, approval связывает согласие с конкретной операцией, а verification читает результат. Если эти доказательства смешать, batch-автоматизация может изменить лишние объекты.", + "contentHtml": "

Симптом обнаруживается после зелёного pipeline: preview показал ожидаемый diff, а reviewer нажал approve. После запуска изменились записи за пределами заявки. В журнале есть operation id, но из него не видно, какие объекты реально изменились. Команда тратит часы на восстановление списка целей и рискует запустить ещё один широкий change под видом rollback.

\n

Причина не в отсутствии ещё одной кнопки подтверждения. Разные этапы отвечают на разные вопросы: preview описывает предложение в определённый момент, authority задаёт предел полномочий, approval фиксирует согласие, audit trail сохраняет событие, а verification читает целевое состояние после записи. Без явной связи между ними зелёный статус доказывает меньше, чем кажется.

\n

Пять артефактов — пять разных утверждений

\n

Начните с разделения доказательств. Это не бюрократия, а способ не выдать одно наблюдение за другое. Наличие preview не говорит, что scope разрешён. Approval не говорит, что target list остался прежним. Receipt от executor не доказывает, что целевая система сохранила значение.

\n
Что доказывает каждый артефакт и какой вопрос остаётся открытым
АртефактЧестное утверждениеОпасная подменаСледующая проверка
PreviewДля зафиксированного snapshot рассчитан proposed changeExecute даст тот же эффектПроверить свежесть и preconditions перед записью
AuthorityPolicy разрешает selector и размер scopeChange полезен и технически корректенСверить intent и содержимое preview
ApprovalReviewer разрешил связанную карточку в своей ролиСогласие переносится на похожую операциюСравнить operation, proposal, target и authority digests
Audit trailExecutor записал событие по своему контрактуTarget system уже в ожидаемом состоянииПрочитать authoritative source после execute
VerificationReader наблюдал заданное expected stateВсе побочные последствия устраненыПроверить residual risk и план recovery
\n

В карточке операции храните как минимум идентификатор, selector, точный список targets, digest списка, ожидаемое состояние до и после, версию логики изменения и срок действия. Человеческое описание остаётся полезным для review, но executor должен сравнивать машинные поля буквально.

\n

Preview — снимок, а не разрешение

\n

Preview устаревает, когда меняется объект, 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.

\n

Сохраняйте не только красивый diff. Практический минимум выглядит так: operationId, snapshotVersion, selector, dense target list, targetDigest, expected precondition и proposalDigest. Digest здесь связывает две фазы и позволяет обнаружить расхождение. Он не заменяет криптографическую подпись, проверку identity или защиту хранилища.

\n

Dry-run имеет конкретную границу

\n

Слово dry-run описывает режим, но не обещает одинаковый объём проверки. В официальной reference для kubectl apply режим --dry-run=client только печатает объект, который был бы отправлен, без отправки. Режим --dry-run=server передаёт запрос на сервер, но не сохраняет ресурс. Значит, client mode проверяет локальную подготовку объекта, а server mode проходит часть серверного пути. Ни один режим сам по себе не подтверждает будущий persistent change.

\n

В карточке записывайте источник данных и границу побочного эффекта: «локальная модель, внешний read не выполняется», «server admission, сохранение запрещено» или «read-only inventory со snapshot». Тогда reviewer понимает, какую проверку ещё нужно сделать. Если этого поля нет, слово preview создаёт ложное ощущение полноты.

\n
Матрица показывает, как authority, target scope, preview digest и approval binding ведут к execute и verification; несовпадение любой границы останавливает операцию
Authority ограничивает scope, approval связывает конкретную карточку, а verification проверяет состояние после execute. Красная клетка означает остановку до следующей записи, а не автоматический rollback.
\n

Authority ограничивает мощность операции

\n

Authority — это машинная граница: допустимый selector, максимальное число targets, срок действия и исключения. Она отвечает на вопрос «можно ли этой роли работать с таким scope», но не решает, правильно ли менять поле. Если policy разрешает два объекта, карточка с тремя должна остановиться до approval. Список нельзя молча уменьшать: reviewer должен видеть ровно тот scope, который получит executor.

\n

Сравнивайте exact selector и exact target digest. Не подменяйте их словами «маленький batch» или «почти тот же список». Не расширяйте authority, если часть целей исчезла из inventory. Не превращайте превышение лимита в warning, когда последующая операция может затронуть больше объектов, чем видел reviewer.

\n

Воспроизводимый gate до записи

\n

Ниже — автономный пример на Node.js. Он работает только с объектами в памяти, не читает сеть, не проверяет реальные права и ничего не меняет во внешней системе. В нём три synthetic target, authority разрешает два, а approval связан с точным operation и target digest. Скопируйте блок в shell с Node.js 18+:

\n
node --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 и срок решения проверяются отдельно.

\n

Approval связывает согласие с карточкой

\n

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 без проверки нельзя.

\n

Bypass тоже является событием с владельцем, причиной и audit record. Если исключение скрыто внутри общего поля approved=true, расследование не отличит обычное согласие от обхода. Для destructive operation отсутствие reviewer или истёкший expiry должны вести к stop, а не к default-решению.

\n

Receipt и verification — разные результаты

\n

Audit trail восстанавливает причинную цепочку: operation id, версии runner и policy, selector, digests, authority, approval, время и результат по каждой цели. Но запись «executor отправил запрос» — это receipt. Она не подтверждает, что target system приняла значение, что controller не откатил его сразу и что побочный объект не изменился.

\n

Verification начинается с ожидаемого состояния. Например: authoritative reader видит label env=staging на двух конкретных targets, digest списка совпадает, а отсутствующие объекты перечислены. У проверки должны быть три различимых исхода: matched, mismatched и unknown. Недоступный reader — это unknown, а не «успех с предупреждением».

\n
Диагностика после запуска
СимптомГипотезаПроверкаДействие
Изменились лишние объектыSelector расширился или scope не был связан с approvalСравнить target digest до write и фактический списокОстановить цепочку и собрать bounded inventory
Preview и execute показывают разный diffTarget 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 как новую операцию
\n

Rollback — новый change

\n

После частичного результата нельзя считать исходное значение безопасной обратной командой. Человек мог исправить часть targets, исходное состояние могло стать устаревшим, а новая policy может запретить обратную запись. Поэтому первый шаг — observed inventory: что изменилось, где состояние неизвестно, какие зависимости затронуты и кто владеет recovery.

\n

Затем rollback проходит тот же bounded маршрут: новый scope, новые preconditions, новая authority, новый preview, отдельный approval и последующая verification. Статус «rollback planned» не означает «rollback executed». Автоматический inverse в блоке finally опасен именно потому, что исполняется без свежего знания о состоянии.

\n

Практический порядок внедрения

\n
  1. Выберите одну операцию. Начните с обратимого batch на двух учебных или тестовых targets, а не с широкого selector.
  2. Опишите contract. Запишите intent, selector, exclusions, exact target list, target digest и expected before/after.
  3. Зафиксируйте preview boundary. Укажите snapshot version, источник, время расчёта и условие, при котором нужен новый preview.
  4. Проверьте authority. Сравните selector, лимит, срок и исключения до запроса approval.
  5. Привяжите approval. Сверьте operation, proposal, target и authority ids буквально; любое отсутствие означает stop.
  6. Повторите preconditions перед write. Проверьте существование targets, версию, scope и действительность policy.
  7. Сохраните receipt отдельно. Не называйте событие executor доказательством состояния.
  8. Сделайте observed verification. Прочитайте authoritative source и зафиксируйте matched, mismatched или unknown.
  9. При unknown остановитесь. Соберите observed inventory и подготовьте новый bounded change; не используйте старое approval для rollback.
\n

Границы применимости

\n

Схема не создаёт identity provider, policy engine, неизменяемое хранилище журнала, распределённую блокировку, гарантию идемпотентности или реальный rollback. Учебный код не доказывает права пользователя, свежесть inventory, криптографическую стойкость digest и фактическое состояние production. Он проверяет только связь полей и отказ до внешней записи.

\n

Даже независимая verification имеет границу: reader может быть устаревшим, неполным или не видеть побочные системы. Для каждого change назовите источник истины, допустимую задержку и владельца unknown. Для удаления и других необратимых действий добавьте отдельные меры: резервную копию, лимит скорости, ручное подтверждение и план восстановления.

\n

Документация Terraform, Kubernetes и GitHub меняется; флаги и доступность функций нужно сверять с версией и тарифом своего окружения. Примеры в статье иллюстрируют узкие свойства официальных инструментов, а не сертифицируют единый безопасный процесс. Готовность определяется собственными тестами: scope выше лимита останавливается, чужой digest не проходит, недоступный reader остаётся unknown, а старое approval не запускает rollback.

\n

Проверяемые источники

\n" } diff --git a/editorial/agent-rewrites/096.json b/editorial/agent-rewrites/096.json index e7284ad..a03c4c1 100644 --- a/editorial/agent-rewrites/096.json +++ b/editorial/agent-rewrites/096.json @@ -1,7 +1,7 @@ { "index": 96, "slug": "editorial-2025-05-practice-engineering-automation", - "title": "Массовая автоматизация без слепого запуска: scope, approval и проверка результата", - "excerpt": "Как превратить массовую инженерную операцию в проверяемую цепочку: зафиксировать targets, ограничить scope, связать approval с preview, отделить receipt от verification и остановиться при несовпадении.", - "contentHtml": "

Скрипт меняет 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 фиксирует намерение, а не обещает эффект

Хороший 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
\"Цепочка
Каждая карточка отвечает на свой вопрос. Красная ветка останавливает операцию; rollback начинается только с нового плана и новой проверки.

Учебная модель с fail-closed поведением

Ниже — учебный пример в памяти. Он не читает 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 изменились. Проверка результата потребовала бы отдельного чтения системы, которой в примере нет.

Dry-run имеет собственную границу

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.

Approval связывает, но не оправдывает

Поле 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 и verification отвечают на разные вопросы

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 сообщает, что внешний объект сейчас выглядит ожидаемым образом. Эти записи нельзя объединять в одно поле.

Rollback — новая операция

Автоматический inverse payload опасен. Target мог измениться вручную после запуска. Исходное значение могло быть неправильным. Часть объектов могла принять новое состояние, а часть — нет. Внешняя зависимость могла изменить порядок восстановления.

При остановке безопасно создать только rollback plan: сохранить причину, прочитать observed state, определить новый bounded scope, выбрать owner, построить новый preview и получить новую authority и approval. Статус rollback-planned не означает rollback-executed. Старое approval не даёт права на новый write.

Порядок действий

  1. Выберите одну операцию. Начните с малого обратимого batch, а не с широкого selector на всей системе.
  2. Назовите exact targets. Запишите ids, selector, exclusions, digest, expected before/after и maximum scope.
  3. Разделите capability. Preview и approval не должны иметь права записи. Execute принимает только связанный approval.
  4. Проверьте отказ. Добавьте лишний target, подмените digest, удалите authority id и передайте неизвестную карточку. Для каждого входа ожидайте STOP.
  5. Привяжите approval. Сравните operation id, selector, target digest, authority id, policy version и срок действия непосредственно перед execute.
  6. Запишите receipt отдельно. Сохраните переход и stop reason. Не называйте receipt подтверждением состояния внешней системы.
  7. Выполните независимую verification. Прочитайте authoritative source, сравните expected и observed, зафиксируйте partial result и задержку.
  8. Остановите неясный результат. При недоступном source или несовпадении не делайте автоматический retry-write.
  9. Планируйте rollback заново. Используйте observed state и новый scope. Получите новое разрешение на отдельную операцию.

Ограничения

Учебная модель не проверяет реальные 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 завершился успешно», а «система показывает, что именно было разрешено, что произошло и каким чтением проверен результат».

Проверяемые источники

" + "title": "Как безопасно автоматизировать массовые изменения: preview, approval и проверка результата", + "excerpt": "Практический маршрут для операций, которые меняют сразу много объектов: зафиксировать состав targets, связать preview с разрешением, проверить отказоустойчивость и отделить receipt исполнителя от фактического состояния системы.", + "contentHtml": "

Скрипт меняет 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 фиксирует намерение, а не обещает эффект

Хороший 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
\"Цепочка
Каждая карточка отвечает на свой вопрос. Красная ветка останавливает операцию; rollback начинается только с нового плана и новой проверки.

Учебная модель с fail-closed поведением

Ниже — учебный пример в памяти. Он не читает 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 изменились. Проверка результата потребовала бы отдельного чтения системы, которой в примере нет.

Dry-run имеет собственную границу

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.

Approval должен быть связан с конкретным preview

Поле 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 и verification отвечают на разные вопросы

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 сообщает, что внешний объект сейчас выглядит ожидаемым образом. Эти записи нельзя объединять в одно поле.

Rollback — новая операция

Автоматический inverse payload опасен. Target мог измениться вручную после запуска. Исходное значение могло быть неправильным. Часть объектов могла принять новое состояние, а часть — нет. Внешняя зависимость могла изменить порядок восстановления.

При остановке безопасно создать только rollback plan: сохранить причину, прочитать observed state, определить новый bounded scope, выбрать owner, построить новый preview и получить новую authority и approval. Статус rollback-planned не означает rollback-executed. Старое approval не даёт права на новый write.

Порядок действий

  1. Выберите одну операцию. Начните с малого обратимого batch, а не с широкого selector на всей системе.
  2. Назовите exact targets. Запишите ids, selector, exclusions, digest, expected before/after и maximum scope.
  3. Разделите capability. Preview и approval не должны иметь права записи. Execute принимает только связанный approval.
  4. Проверьте отказ. Добавьте лишний target, подмените digest, удалите authority id и передайте неизвестную карточку. Для каждого входа ожидайте STOP.
  5. Привяжите approval. Сравните operation id, selector, target digest, authority id, policy version и срок действия непосредственно перед execute.
  6. Запишите receipt отдельно. Сохраните переход и stop reason. Не называйте receipt подтверждением состояния внешней системы.
  7. Выполните независимую verification. Прочитайте authoritative source, сравните expected и observed, зафиксируйте partial result и задержку.
  8. Остановите неясный результат. При недоступном source или несовпадении не делайте автоматический retry-write.
  9. Планируйте rollback заново. Используйте observed state и новый scope. Получите новое разрешение на отдельную операцию.

Ограничения применимости

Учебная модель не проверяет реальные 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 завершился успешно», а «система показывает, что именно было разрешено, что произошло и каким чтением проверен результат».

Проверяемые источники

" } diff --git a/editorial/agent-rewrites/097.json b/editorial/agent-rewrites/097.json index b2c2ba4..46b2db5 100644 --- a/editorial/agent-rewrites/097.json +++ b/editorial/agent-rewrites/097.json @@ -1,7 +1,7 @@ { "index": 97, "slug": "editorial-2025-04-field-ai-data-privacy", - "title": "Перед AI-инструментом данные должны пройти четыре проверки", - "excerpt": "Почему удаление имён из лога не делает контекст безопасным: разбираем классификацию, договорные условия, маршрут передачи и полномочия на конкретный фрагмент.", - "contentHtml": "

Инженер копирует в AI-чат фрагмент ошибки, удаляет имя пользователя и ждёт подсказку. Симптом исчезает из текста, но риск остаётся. В логе могут сохраниться время операции, редкий тип события, идентификатор запроса, структура платежа или сочетание полей, по которому запись узнают. Если сервис добавляет к prompt историю диалога, файлы проекта или поиск, наружу уходит не только выделенная строка.

\n

Цена ошибки складывается из нескольких частей. Команда теряет контроль над копией данных. Нельзя быстро доказать, куда она попала и как долго хранилась. Нельзя уверенно ответить на запрос владельца данных. В худшем случае один удобный ответ превращается в инцидент, расследование и остановку инструмента для всей команды.

\n

Тезис

\n

Безопасный AI-review начинается не с маскирования и не с выбора модели. Сначала нужно доказать, что конкретный фрагмент можно передать конкретной поверхности по конкретному маршруту. Для этого разделите четыре решения: какой класс у данных, какие условия обработки действуют, куда пойдёт запрос и кто имеет право его отправить. Если хотя бы один факт неизвестен, путь должен закрыться.

\n

Эта граница важнее названия продукта. Один vendor может иметь несколько поверхностей: веб-чат, расширение редактора, API, поиск и агент с доступом к репозиторию. Они могут собирать разный контекст и использовать разные настройки хранения. Одобрение «AI разрешён» не покрывает автоматически каждый экран и каждый тип записи.

\n

Как возникает ошибка

\n

Входной фрагмент обычно рассматривают как строку. Система должна рассматривать его как объект с контекстом: recordId, версией, владельцем, классом, целью, destination и сроком действия разрешения. Redaction меняет содержимое. Он не назначает класс и не подтверждает договорные условия. Поле, заменённое на [redacted], всё ещё может быть чувствительным по структуре.

\n

Следующий слой — маршрут. Запрос может пройти через прокси, региональный endpoint, подключённый поиск, плагин или сервис-посредник. Успешное TLS-соединение доказывает только доступность канала. Оно не доказывает, что выбранный destination разрешён для этого класса данных. Так же firewall allow-list не превращает restricted record в public.

\n

Последний слой — полномочия. Разрешение относится к ресурсу, цели, requester и сроку. Старое согласование для синтетического примера не даёт права отправить production-лог. Доступ к инструменту не равен праву передавать ему все доступные пользователю данные.

\n

Учебный пример: запрос, который обязан остановиться

\n

Ниже приведён синтетический пример. Он не читает лог, не содержит PII, секретов, клиентских идентификаторов и настоящего endpoint. Названия намеренно фиксированы. Цель примера — показать отрицательный путь: при restricted-классе функция закрывает передачу до проверки разрешения. Это не готовая DLP-система и не доказательство безопасности какого-либо AI-сервиса.

\n
const 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

Симптом → причина → проверка → действие

\n
Диагностика передачи контекста в AI-инструмент
СимптомПричинаПроверкаДействие
«Мы удалили имена, значит всё можно»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Считать результат недоказанным до проверки маршрута
\n

Граница передачи

\n
\"Схема
Учебная схема показывает порядок вопросов. Она не изображает реальный сетевой trace и не подтверждает настройки конкретного поставщика.
\n

На схеме нет шага «попросить модель оценить риск». Модель может помочь сформулировать вопрос, но не должна сама становиться источником разрешения на доступ к данным. Сначала работает детерминированный gate. Он возвращает один из двух исходов: hand-off в заранее разрешённый workflow или stop с причиной и владельцем следующего решения.

\n

Порядок действий

\n
  1. Зафиксируйте объект. Запишите record ID, версию, владельца и минимально необходимую цель. Не вставляйте исходный payload в заявку на согласование.
  2. Назначьте класс. Проверьте, какое правило относится к данным после всех преобразований. Если class неизвестен, считайте его неизвестным, а не public.
  3. Проверьте условия обработки. Найдите документ, версию, plan, регион, retention, training terms и подключённые функции для этой поверхности. Не переносите вывод с другого продукта или аккаунта.
  4. Опишите маршрут. Укажите endpoint или сервис, прокси, поиск, расширения и другие места, куда может уйти context. Отдельно проверьте, что технический маршрут разрешён для этого класса.
  5. Проверьте полномочия. Сопоставьте requester, цель, record, destination, issuer и дату окончания. Устаревшее или широкое approval не расширяйте по аналогии.
  6. Выберите отрицательный путь. При пробеле оставьте данные локально, верните stop с причиной и назначьте владельца вопроса. Не обходите запрет через другой account, устройство или неофициальный интерфейс.
  7. Проверьте минимальный тест. Запустите synthetic case для разрешённого и запрещённого классов. Убедитесь, что stop не строит prompt и не вызывает внешний клиент.
\n

Ограничения

\n

Эта схема не заменяет инвентаризацию данных, DLP, договор, privacy impact assessment, контроль доступа и аудит поставщика. Она не обнаруживает PII сама по себе. Она не знает, что делает vendor после получения запроса, если это не подтверждено условиями конкретного сервиса. Она также не решает вопрос законного основания обработки и не определяет допустимость данных без владельца policy.

\n

Маскирование может снизить объём данных, но не гарантирует анонимизацию. Локальная модель может убрать внешний egress, но не отменяет права доступа и хранение локальных журналов. Человеческое согласование полезно, но не должно заменять проверяемые ограничения в коде и конфигурации.

\n

Учебные строки в примере не дают production-результата. Они показывают форму контракта. В реальной системе положительный результат нужно подтвердить документацией поставщика, конфигурацией маршрута, журналом события и решением владельца данных на дату запуска.

\n

Проверяемый критерий готовности

\n

Решение готово, если другой инженер без доступа к исходному обсуждению может повторить проверку и получить тот же исход. Для одного synthetic restricted record тест должен показать: внешний вызов не выполнен, payload не попал в prompt builder, причина stop сохранена, а следующий владелец назван. Для допустимого учебного record тест должен показать все совпадения: class, условия обработки, destination, requester и expiry.

\n

Если хотя бы один из этих фактов нельзя предъявить ссылкой, конфигурацией или тестовым результатом, передача не доказана. Оставьте данные локально и закройте вопрос до появления недостающего evidence.

\n

Проверяемые источники

\n" + "title": "Почему замаскированный лог всё ещё нельзя отправлять в AI-инструмент", + "excerpt": "Разбираем синтетический кейс: как отделить класс данных, условия сервиса, сетевой маршрут и полномочия, а затем воспроизвести безопасную остановку без внешнего запроса.", + "contentHtml": "

Инженер копирует в AI-чат фрагмент ошибки, заменяет имя на [redacted] и ждёт подсказку. На вид в тексте больше нет персональных данных. Но рядом могут остаться время операции, редкий тип события, идентификатор запроса, структура платежа или сочетание полей, по которому запись легко узнать. А сама поверхность может добавить к запросу историю диалога, файл проекта, поиск или другой контекст.

\n

Симптом здесь не в том, что AI дал плохой ответ. Проблема обнаруживается позже: команда не может доказать, какой фрагмент ушёл, к какому сервису, на каких условиях и с чьего разрешения. Цена такой неопределённости — повторная инвентаризация, остановка полезного сценария и риск раскрытия персональных, финансовых или служебных данных. Маскирование уменьшает содержимое, но не превращает его автоматически в разрешённый контекст.

\n

Кейс: «это всего один лог»

\n

Рассмотрим учебную ситуацию. Разработчик хочет показать ассистенту форму production-события и один тикет, чтобы найти причину повторной ошибки. В статье нет настоящего лога, клиента, секрета, идентификатора, файла или сетевого адреса. Вместо них используются фиксированные строки с пометкой synthetic. Это позволяет проверить границу решения, не передавая наружу даже учебный payload.

\n

Первое ложное упрощение звучит так: «имена удалены, значит данные обезличены». Для этого вывода нужно отдельно проверить остаточную структуру и риск повторной идентификации. Второе: «у нас корпоративный тариф, значит разрешён любой экран». У одного поставщика могут отличаться web-чат, расширение редактора, API, поиск и агент с доступом к репозиторию. Третье: «домен разрешён firewall, значит передачу одобрили». Сеть отвечает за маршрут, а не за классификацию записи и права человека.

\n

Четыре независимых вопроса

\n

Перед отправкой полезно описывать не строку, а одну единицу контекста. У неё есть идентификатор, версия, владелец, класс, назначение, destination, requester и срок действия решения. Если поле неизвестно, его нельзя заполнять оптимистичным значением public. Нулевое знание должно вести к остановке.

\n
Что проверяет каждый слой решения
СлойВопросМинимальное доказательствоЧего он не доказывает
КлассЧто представляет запись после преобразования?Версия классификации, owner и описание остаточной структурыУсловия хранения у поставщика и право на egress
Policy и contractДопустим ли этот класс для выбранной surface?Документ и версия условий для конкретного plan, региона и функцииЧто фактически попало в запрос и кто его отправляет
МаршрутКуда может уйти запрос и дополнительный context?Surface, destination, proxy или gateway и техническое правилоБезопасность payload и полномочия requester
ПолномочияКто может использовать именно эту запись сейчас?Scoped approval: record, цель, requester, issuer и expiryРазрешение всех будущих фрагментов и интерфейсов
\n

Эти слои пересекаются в одной операции, но не заменяют друг друга. Даже проверенный договор не классифицирует конкретный лог. Даже одобренный класс не открывает неизвестный endpoint. Даже доступ к AI-инструменту не означает право раскрывать все данные, которые видит пользователь. Итоговое решение должно ссылаться на все четыре слоя или закрывать путь.

\n

Почему маскирование не равно анонимизации

\n

Redaction — это преобразование значения. Классификация отвечает на другой вопрос: какой риск остаётся у результата. Например, замена номера клиента на [redacted] не скрывает редкую последовательность действий, точное время и вид операции. Небольшой набор таких признаков может сузить поиск до одной записи. Поэтому в карточке нужно фиксировать не только удалённые поля, но и то, что осталось.

\n

Не следует и автоматически называть результат анонимным. Статья не предлагает юридический тест анонимизации: его критерии зависят от применимого права, целей обработки и возможностей сопоставления. Инженерная граница проще: пока владелец данных не подтвердил класс и допустимость, фрагмент не отправляется. Для отладки лучше начать с заранее подготовленного примера, схемы события или локального fixture, если именно они отвечают на вопрос.

\n

Почему разрешённая сеть не даёт права на данные

\n
\"Схема
Схема разделяет evidence и действие: положительный результат означает готовность к отдельному согласованному workflow, а не выполненный внешний запрос.
\n

Сетевой allow-list полезен, но его область действия узкая. Он может разрешить соединение с определённым destination или направить его через прокси. Он не анализирует смысл каждого поля, не знает срок действия approval и не решает, разрешена ли выбранная surface условиями организации. TLS подтверждает защищённость канала от подмены при передаче, но не делает саму передачу допустимой.

\n

Такой разрыв хорошо согласуется с принципом zero trust из NIST SP 800-207: доверие не должно следовать только из сетевого расположения, а authentication и authorization — отдельные функции перед доступом к ресурсу. Для AI-review это означает: факт «с рабочего ноутбука домен открывается» нельзя использовать как замену проверке requester, record и назначения.

\n

Воспроизводимый fail-closed пример

\n

Ниже — автономный Node.js-фрагмент. Он создаёт две синтетические карточки в памяти, проверяет класс, destination, requester, approval и срок, а затем печатает решение. Код не читает файлы, не строит prompt и не выполняет HTTP-запрос. Его можно сохранить в временный файл и запустить командой из блока; ожидаемые строки показывают только поведение локального правила.

\n
node --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 смысл: неизвестное условие не становится разрешением.

\n

Разбор симптома по цепочке

\n
Диагностика перед AI-review
НаблюдениеГипотезаПроверкаДействие
Имена удаленыОстаточная структура безопаснаСверить class после преобразования, редкие поля и ownerПри неизвестном классе остановить передачу
Продукт разрешёнВсе его поверхности одинаковыНазвать точные feature, plan, region и destinationСверить условия именно этой surface
Firewall пропускает доменТехнический egress равен праву на данныеСопоставить route с class и policyНе использовать сеть как approval
Есть согласование командыОно покрывает любой контекстПроверить record, цель, requester, issuer и expiryЗапросить scoped решение или остаться локально
Ассистент ответилВнешнего раскрытия не былоПроверить outbound request, дополнительные context sources и retention termsСчитать результат недоказанным до проверки маршрута
\n

Таблица нужна не для бюрократии. Она не даёт одному удачному факту закрыть соседний вопрос. Если команда не может назвать destination, это отдельный пробел маршрута. Если destination известен, но нет scoped approval, это отдельный пробел полномочий. Такой разбор уменьшает спор о бренде инструмента и показывает, кому адресовать следующий вопрос.

\n

Короткий pre-flight review

\n
  1. Зафиксируйте запись. Укажите record ID, версию, владельца и минимальную цель. Не помещайте исходный payload в заявку на согласование.
  2. Назначьте класс. Проверьте результат после маскирования и сохраните правило, по которому он классифицирован. Не называйте неизвестное значение public.
  3. Сверьте условия сервиса. Откройте документ для конкретной поверхности, тарифа, региона и функции. Отдельно проверьте retention, обучение и подключённый поиск, если они относятся к вашему решению.
  4. Опишите маршрут. Запишите destination, прокси, gateway, расширение и дополнительные источники context. Убедитесь, что технический путь разрешён для этого класса.
  5. Проверьте полномочия. Сопоставьте requester, record, цель, issuer и expiry. Доступ к инструменту не расширяйте до доступа ко всем данным пользователя.
  6. Прогоните отрицательный тест. Подставьте запрещённый класс, истёкший срок и другой destination. Во всех случаях ожидайте stop, причину и отсутствие внешнего вызова.
  7. Передайте только решение. При полном evidence отправьте карточку в согласованный workflow. При пробеле оставьте данные локально и назначьте владельца вопроса.
\n

Ограничения применимости

\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.

\n

Проверяемый результат

\n

Review можно считать завершённым, когда другой инженер повторяет его без доступа к исходному обсуждению. Для restricted-карточки тест должен показать stop до вызова клиента, отсутствие payload в prompt builder, сохранённую причину и назначенного владельца следующего вопроса. Для разрешённой учебной карточки должны совпасть class, условия, destination, requester, issuer и expiry. Это не подтверждает правду о vendor; это подтверждает, что ваша локальная процедура не принимает неизвестное за известное.

\n

Практический следующий шаг — взять один тип рабочего контекста, но не копировать его в новый документ. Заполните пустую карточку четырьмя блоками: class и owner; policy и contract; маршрут; полномочия и срок. Затем намеренно оставьте один блок пустым и убедитесь, что workflow останавливается без payload. Если причина и следующий владелец не появляются в результате, сначала исправьте review-контур.

\n

Проверяемые источники

" } diff --git a/editorial/agent-rewrites/098.json b/editorial/agent-rewrites/098.json index a9e4be1..294cb7c 100644 --- a/editorial/agent-rewrites/098.json +++ b/editorial/agent-rewrites/098.json @@ -1,7 +1,7 @@ { "index": 98, "slug": "editorial-2025-04-mechanism-ai-data-privacy", - "title": "Передача данных в AI: четыре проверки до отправки контекста", - "excerpt": "Класс данных, условия обработки, сетевой маршрут и полномочия отвечают на разные вопросы. Разбираем, как соединить их в один fail-closed контроль и остановить передачу при пробеле в доказательствах.", - "contentHtml": "

Симптом обычно выглядит безобидно: инженер копирует в AI-инструмент кусок лога, тикет или stack trace, а потом не может точно сказать, что именно ушло наружу. В настройках включён корпоративный тариф, сеть разрешает нужный домен, коллега устно подтвердил «можно». Но эти факты не доказывают одно и то же. Цена ошибки — раскрытие персональных данных или секрета, нарушение договорного условия, невозможность восстановить решение и остановка всего сценария до повторной проверки.

\n

Тезис статьи простой: передача контекста допустима только тогда, когда сходятся четыре независимых доказательства. Нужно знать класс записи, применимую policy и договорные условия, конкретный технический маршрут и полномочия человека на эту запись в этот срок. Если один слой неизвестен, система должна остановить hand-off и вернуть причину. Нельзя заменить отсутствующий факт более удобным фактом из другой колонки.

\n

Почему одного разрешения недостаточно

\n

Класс описывает содержимое. Например, public, internal или restricted. Это не название папки и не ощущение автора фрагмента. Класс должен иметь владельца и правило, по которому его присвоили. Удаление имени из строки тоже не меняет автоматически класс: структура события, редкий идентификатор и сочетание полей могут оставаться чувствительными.

\n

Policy отвечает на вопрос «допустима ли такая обработка». Договор уточняет условия для конкретного сервиса, тарифа, региона и функции: retention, обучение моделей, subprocessors или запрет внешнего хранения. Слово «корпоративный» не заменяет проверку этих условий. Одобрение продукта не является автоматически разрешением на любой payload.

\n

Egress отвечает на другой вопрос: куда и каким путём может уйти запрос. Это endpoint, proxy, firewall, DNS-политика или другой сетевой control. Разрешённый маршрут не делает данные допустимыми. Authorization отвечает ещё на один слой: какой владелец разрешил конкретную запись, конкретному requester, для конкретного destination и до какой даты.

\n
\"Маршрут
Каждый слой проверяет свой вопрос. Несовпадение на любом переходе закрывает передачу и сохраняет причину остановки.
\n

Механизм: четыре доказательства должны описывать одну операцию

\n

Рассмотрим запрос на анализ ошибки в платёжном сервисе. Инженер хочет передать AI фрагмент журнала. В нём есть время, тип операции, идентификатор клиента и текст исключения. Даже если значение идентификатора заменили на client-17, нужно отдельно решить, что осталось в записи и какой класс ей присвоен.

\n

Первое доказательство — карточка контекста. В ней есть стабильный идентификатор записи, версия, класс, владелец, срок действия и ссылка на правило классификации. Второе — policy и contract evidence для выбранной поверхности. Ссылка должна покрывать именно тот plan и тот режим, в котором работает инструмент. Третье — проверенный egress scope: destination и технический control должны совпадать с карточкой. Четвёртое — датированное решение владельца. Оно содержит record id, requester, destination, срок и причину.

\n

Проверка не должна начинаться с approval. Иначе человек сможет невольно «перекрыть» запрет класса. Она не должна начинаться с сети. Иначе доступный endpoint станет ложным доказательством допустимости. Сначала проверяют форму запроса и запись, затем допустимость, маршрут, доступ и срок полномочий. Только после этого можно передать результат в отдельный разрешённый workflow.

\n

Таблица диагностики

\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
\n

Учебный пример с отрицательным путём

\n

Ниже — ограниченный пример на JavaScript. В нём используются только фиксированные строки, нет настоящего лога, PII, секрета, файла, сети, вызова модели или production side effect. Функция не отправляет payload. Она показывает только контракт: запись с запрещённым классом должна остановиться до проверки approval.

\n
const 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, причина, ссылка на проверенный источник и следующий владелец вопроса. Это не доказывает, что технология всегда запрещена. Это доказывает только, что текущих фактов недостаточно для данной передачи.

\n

Как выполнить проверку

\n
  1. Остановите копирование. Назовите record id и не открывайте AI-поверхность ради «быстрой проверки».
  2. Опишите класс. Зафиксируйте содержимое, версию классификации, owner и правило. Не считайте редактирование доказательством безопасности.
  3. Сверьте policy и contract. Проверьте plan, feature, region, retention и другие условия, которые относятся к выбранной surface.
  4. Проверьте маршрут. Запишите destination, route и сетевой control. Убедитесь, что они совпадают с разрешённым scope.
  5. Проверьте полномочия. Сопоставьте requester, access к record, роль владельца, record id и expiry. Старое общее одобрение не переносится автоматически.
  6. Выберите outcome. При полном совпадении передайте только разрешённый контекст в authorised workflow. При пробеле сохраните payload локально, запишите reason и назначьте следующий вопрос.
\n

Что могут и чего не могут доказать официальные документы

\n

Документация конкретного AI-продукта может описывать обработку prompt, добавление repository context или отдельный внешний поиск. Это помогает обнаружить дополнительные границы egress. Но такая документация не классифицирует ваш лог и не выдаёт сотруднику право на его раскрытие.

\n

Сетевое правило снижает риск неправильного endpoint, но не видит смысл payload. DPA и условия сервиса помогают ответить на вопрос о permitted processing, но не подтверждают, что человек имеет доступ к записи. NIST описывает policy-based access и неявное доверие к network location как разные вещи. Эти источники формируют вопросы для проверки; они не дают универсального вердикта «можно передавать».

\n

Ограничения

\n

Четыре проверки не заменяют DLP, data inventory, юридическую оценку, access control или технический мониторинг. Классификация может ошибиться. Документ поставщика может измениться. Proxy может быть настроен не так, как написано в схеме. Поэтому в рабочей системе храните версию evidence, дату проверки, применимый scope и границу, которую документ не покрывает.

\n

Не обещайте результат, которого не измеряли. Эта статья не утверждает, что конкретная интеграция сохраняет или не сохраняет prompt, не обучает на нём модели и не гарантирует отсутствие утечки. Учебный код не является доказательством свойств production. Его проверяемый результат уже: запрещённый класс не приводит к передаче.

\n

Критерий готовности

\n

Контроль готов к ограниченному применению, когда другой инженер может взять одну карточку без payload и воспроизвести весь вопрос: какой класс, какая policy и договорная версия, какой destination и route, кто разрешил, какой срок. Для каждого пробела система возвращает accepted: false, reason, citation и next action; ни один отрицательный case не вызывает внешний запрос. Положительный outcome допускает только тот scope, который совпал во всех четырёх доказательствах. Если эти условия нельзя проверить, готов не hand-off, а только следующий вопрос владельцу данных.

\n

Проверяемые источники

\n" + "title": "Данные и приватность в AI-инструментах: как принять инженерное решение", + "excerpt": "Передача контекста в AI — это не одно разрешение, а проверяемая цепочка из класса данных, условий обработки, маршрута и полномочий. Разбираем fail-closed контроль с воспроизводимым отрицательным примером.", + "contentHtml": "

Симптом появляется в момент, когда инженер хочет ускорить разбор ошибки: он копирует в AI-инструмент фрагмент лога, тикет или stack trace, а потом не может точно восстановить, что стало контекстом запроса. Корпоративный тариф, разрешённый домен и устное «можно» отвечают на разные вопросы. Ни один из этих фактов сам по себе не доказывает допустимость передачи.

\n

Цена ошибки измеряется не только возможной утечкой. В журнале могут остаться идентификатор клиента, редкое сочетание событий, токен, внутреннее имя сервиса или сведения, по которым запись легко сопоставить с человеком. После отправки нужно ещё установить, какой провайдер получил запрос, какие дополнительные данные добавила поверхность и какое решение владельца это разрешало. Поэтому решение принимаем до hand-off (передачи контекста), а при неполной проверке останавливаемся.

\n

Один запрос — четыре независимых вопроса

\n

Удобно разложить передачу на четыре слоя. Первый слой — класс данных: что находится в записи и по какому правилу она получила метку public, internal или restricted. Класс принадлежит конкретной записи, а не папке и не ощущению автора. Маскирование имени не доказывает обезличивание: время, сумма, редкий идентификатор и последовательность событий могут сохранить связь с субъектом.

\n

Второй слой — допустимость обработки. Здесь проверяют политику компании, договор с поставщиком и фактическую функцию: план, регион, режим, хранение, обучение моделей, subprocessors (привлечённых обработчиков) и журналирование, если они относятся к выбранной поверхности. Слово «корпоративный» — только название тарифа. Оно не заменяет чтение применимых условий.

\n

Третий слой — egress, то есть технический выход запроса. Он включает destination (конечный адрес), прокси, API-шлюз, DNS и сетевые правила. Разрешённый firewall-маршрут доказывает лишь возможность соединения. NIST прямо отделяет защиту ресурса и проверку идентичности от доверия к сетевому расположению, поэтому доступный домен не превращается в разрешение на данные.

\n

Четвёртый слой — полномочия. Нужно связать решение с record id, requester (инициатором), ролью владельца, destination, целью и сроком действия. Старое одобрение «для логов» может относиться к другой записи, другой функции или уже истечь. Без этой связи нельзя доказать, что именно данный человек имел право отправить именно этот контекст именно в этот маршрут.

\n
\"Схема
Слои проверяют разные свойства одной операции. Любое расхождение переводит запрос в безопасную остановку без внешней отправки.
\n

Как связать доказательства в один контракт

\n

Рассмотрим учебный запрос на объяснение ошибки в платёжном сервисе. В исходной записи есть время события, тип операции, идентификатор клиента и текст исключения. Инженер заменил идентификатор на client-17, но это лишь преобразование значения. Решение о классе должно учитывать весь остаточный набор полей и правило классификации.

\n

Для проверки заведём карточку операции. Она не обязана содержать сам лог: достаточно ссылки на запись и метаданных, по которым другой инженер сможет повторить решение. Минимальный состав такой: record.id, версия классификации, класс, владелец, версия policy, выбранная функция, destination, сетевой control, requester, цель и expiresAt. Хэш или внутренний идентификатор payload полезен для связи событий, но не должен превращаться в новый журнал с исходными персональными данными.

\n
Четыре слоя решения: что проверяем и чего не следует из проверки
СлойДоказательствоПроверяемый вопросЧего недостаточно
Класс данныхМетка, версия правила, владелец классификацииЧто именно описывает запись и какие ограничения к ней применимы?Удалённое имя или уверенность автора, что фрагмент «безопасный»
Policy и договорВерсия условий для плана, функции и регионаДопускает ли выбранная поверхность такую обработку?Название тарифа или общая страница о продукте без нужного scope
EgressDestination, маршрут и сетевой controlКуда уйдёт запрос и какие компоненты добавят данные?Открытый домен, зелёный firewall или локальный прокси без трассировки
ПолномочияRecord id, requester, owner decision и expiryКто и до какого момента разрешил эту операцию?Устное согласие, общий доступ к проекту или старый approval
\n

Важно проверять не четыре флага по отдельности, а их пересечение. Разрешённый класс и подходящий договор не помогают, если запрос ушёл через BYOK-провайдера, которого не покрывает договор. Правильный маршрут и действующий доступ также не спасают restricted-запись, если правило классификации запрещает внешнюю обработку. В журнале решения полезно сохранить результат каждой проверки и причину отказа, но не копировать исходный payload.

\n

Почему поверхность AI расширяет маршрут данных

\n

У пользователя перед глазами может быть одно поле prompt, а система соберёт больше. В официальном описании GitHub Copilot Chat указано, что prompt объединяется с дополнительным контекстом: открытыми файлами, репозиторием и историей чата; на некоторых поверхностях может добавляться веб-поиск. Значит, перед проверкой надо назвать не только текст, который человек собирается вставить, но и функцию, режим, открытые вкладки, индексированные источники и внешние инструменты.

\n

Это меняет порядок диагностики. Если инженер отправил stack trace из закрытого репозитория, нужно проверить не только строку с ошибкой, но и автоматически выбранные файлы. Если включён веб-поиск, отдельным destination становится поисковый API. Если используется BYOK, GitHub предупреждает, что prompt и ответы передаются выбранному провайдеру и могут подпадать под его правила хранения и приватности. На практике это означает: маршрут нельзя вывести из интерфейса; его нужно подтвердить документацией и конфигурацией конкретной поверхности.

\n

Контентные исключения и индексирование репозитория тоже имеют узкую роль. Настройка, запрещающая инструменту читать определённые файлы, уменьшает доступный контекст. Она не классифицирует остальные записи, не выдаёт пользователю право на отправку и не отменяет договорные ограничения. Так один control снижает конкретный путь утечки, но не закрывает четыре слоя целиком.

\n

Воспроизводимый отрицательный пример

\n

Ниже — самостоятельный фрагмент JavaScript для Node.js без сети, файлов и вызова модели. Он проверяет контракт на фиксированных строках. В примере restricted не входит в список классов, разрешённых выбранной policy, поэтому результат должен быть отрицательным. Сохраните код в handoff-check.mjs и выполните node handoff-check.mjs.

\n
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 из вывода: нужно изменить класс на разрешённый и заново проверить все четыре условия. В рабочем коде добавьте строгую валидацию схемы, запрет неизвестных полей и отдельный тест для каждой причины отказа.

\n

Порядок проверки перед hand-off

\n
  1. Зафиксируйте границу. Назовите record id, выбранную AI-поверхность и цель. Не вставляйте исходный payload в чат для предварительной проверки.
  2. Опишите данные. Укажите состав записи, класс, версию правила, владельца и результат маскирования. Если классификация неизвестна, остановите операцию.
  3. Проверьте условия обработки. Сопоставьте план, функцию, регион, хранение, обучение, subprocessors и срок действия договора. Если документ не покрывает нужный режим, не переносите вывод из похожего режима.
  4. Постройте карту egress. Запишите endpoint, прокси, API-шлюз, DNS, веб-поиск, BYOK и инструменты, которые могут добавить контекст. Для каждого выхода нужен отдельный разрешённый scope.
  5. Сверьте полномочия. Проверьте доступ requester к записи, решение владельца, record id, destination, цель и expiry. Общее право на репозиторий не равно праву отправлять его содержимое наружу.
  6. Сделайте safe stop или hand-off. При любом пробеле верните accepted: false, стабильную причину, ссылку на evidence и следующий вопрос владельцу. При полном совпадении передайте минимальный разрешённый контекст и сохраните событие без исходного текста.
\n

Что журналировать и как проверять контроль

\n

Журнал контроля должен помогать восстановить решение, но не становиться вторым каналом утечки. Для каждого запроса можно записать время, идентификатор операции, хэш или ссылку на запись, версии policy и классификации, выбранную поверхность, destination, результат четырёх проверок и причину остановки. Секреты, полный prompt и ответы модели в такой журнал не попадают. Срок хранения самого журнала выбирается по внутренней политике и договору, а не копируется из этого примера.

\n

Проверка начинается с отрицательных случаев. Подайте тестовую запись класса restricted, просроченное разрешение, другой destination и неизвестную policy. У каждого запуска ожидайте accepted: false; сетевой мониторинг должен показать ноль внешних запросов. Затем отдельно проверьте разрешённый учебный класс и убедитесь, что передача доступна только при совпадении record id, маршрута и срока. Такой тест проверяет поведение шлюза, но не доказывает свойства поставщика после получения запроса.

\n

Для периодического контроля сравнивайте фактический маршрут с карточкой операции: destination из журнала приложения, адрес прокси и включённые функции должны образовывать одну версию конфигурации. Расхождение — это отказ контроля, а не повод автоматически расширить allowlist. Сначала изолируйте операцию и назначьте владельца исправления, затем повторите проверку.

\n

Ограничения применимости

\n

Четыре слоя — инженерная модель принятия решения, а не универсальная сертификация. Она не заменяет DLP, инвентаризацию данных, контроль доступа, юридическую оценку, аудит поставщика или требования конкретной юрисдикции. NIST описывает рамки управления риском и zero trust, но не классифицирует ваш лог и не отвечает за условия вашего договора.

\n

Официальная документация продукта может измениться, а одна и та же функция может работать по-разному в GitHub.com, IDE, CLI и корпоративной конфигурации. Проверяйте дату, версию и surface, на которой действительно работает запрос. Даже доказанный маршрут не показывает, как модель интерпретирует контекст, и не гарантирует корректность ответа. Для критичного кода нужны human review, тесты и независимая проверка результата.

\n

Маскирование снижает объём раскрываемых данных, но не делает любую запись публичной. Локальная модель уменьшает внешний egress, однако оставляет риски доступа, хранения, журналирования и компрометации хоста. Поэтому статья не отвечает «можно ли всегда отправлять логи». Она задаёт более узкий проверяемый критерий: можно ли доказать допустимость этой записи, этой функции, этого маршрута и этого полномочия в момент операции.

\n

Критерий готовности решения

\n

Решение готово к ограниченному применению, если другой инженер без исходного payload может взять карточку и воспроизвести четыре ответа: какой класс, какая policy и договорная версия, какой egress, кто разрешил и до какого срока. Для каждого отрицательного case известны код причины, evidence и следующий владелец вопроса. Ни один такой case не запускает внешний запрос. Если хотя бы один ответ строится на слове «обычно», интерфейсе без трассировки или старом согласовании, готов не hand-off, а новая проверка.

\n

Проверяемые источники

\n" } diff --git a/editorial/agent-rewrites/099.json b/editorial/agent-rewrites/099.json index 4b8df08..78a3c76 100644 --- a/editorial/agent-rewrites/099.json +++ b/editorial/agent-rewrites/099.json @@ -2,6 +2,6 @@ "index": 99, "slug": "editorial-2025-04-practice-ai-data-privacy", "title": "Перед AI-инструментом данные должны пройти четыре независимые проверки", - "excerpt": "Как не отправить код, лог или тикет за пределы компании по ошибке: разделяем класс данных, условия сервиса, сетевой маршрут и полномочия человека.", - "contentHtml": "

Инженер копирует в AI-инструмент десять строк лога, чтобы быстрее найти причину ошибки. В строках оказываются email, внутренний идентификатор и часть заголовка запроса. Интерфейс принимает текст. Ответ выглядит полезным. Через неделю никто не может точно сказать, какой контекст ушёл, к какому режиму сервиса он относился и кто разрешил передачу. Симптом проявился как удобная подсказка, а цена ошибки — потеря контроля над данными и дорогое расследование без исходного payload.

\n

Проблема не решается одной настройкой «корпоративный тариф» или запретом на секреты в prompt. Передача должна пройти четыре независимые проверки: что находится во фрагменте, допускает ли policy и договор такую обработку, куда технически пойдёт запрос и кто разрешил именно этот объём данных. Если один вопрос подменяет остальные, команда получает ложное зелёное состояние.

\n

Тезис: класс данных не равен разрешению

\n

Класс описывает содержимое. Например, public, internal или restricted. Он не говорит, можно ли передавать запись внешнему сервису. Это решает policy с учётом договора, режима хранения, обучения, региона и выбранного endpoint. Даже если policy разрешает класс, сеть должна вести запрос в нужное место, а сотрудник должен иметь право передать конкретный record.

\n

Разделяйте эти факты в карточке контекста. Запишите идентификатор и версию записи, класс, основание допустимости, проверенное условие сервиса, символическое имя назначения, requester, владельца и срок разрешения. Не заполняйте неизвестное значение словом «внутреннее». Не переносите approval с соседнего файла. При неполном evidence путь закрывается.

\n
\"Пять
Передача допускается только после независимой проверки содержимого, условий сервиса, маршрута и полномочий. Отсутствие одного доказательства ведёт к остановке.
\n

Как возникает утечка контекста

\n

Пользователь видит поле prompt, но поставщик может обрабатывать его вместе с контекстом продукта. Это может быть открытый файл, история диалога, выбранный репозиторий или дополнительный поиск. Поэтому проверяйте не только видимый текст, но и конкретную поверхность, включённые функции и endpoint. Такой анализ относится к режиму продукта, а не ко всем AI-инструментам сразу.

\n

Сетевое правило отвечает на узкий вопрос: может ли трафик достичь назначения. Allow-list не классифицирует payload. DPA или другая договорённость описывает обязательства, но не доказывает, что firewall отправил запрос только в нужный endpoint. Разрешение владельца показывает полномочия, но не меняет класс записи и не продлевает истёкший срок. Каждый контроль должен иметь собственную проверку.

\n

Учебный пример: решение без внешнего запроса

\n

Ниже приведён учебный JavaScript-пример. Он работает только с заранее заданными объектами в памяти. Он не вызывает модель, не читает файл, не отправляет лог и не утверждает ничего о production. Его задача — показать порядок проверок и отрицательный путь.

\n
const 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. В реальной системе нужны собственные справочники, журнал решения и проверка фактического маршрута.

\n

Симптом → причина → проверка → действие

\n
Диагностика передачи контекста
СимптомПричинаПроверкаДействие
В 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
\n

Порядок pre-flight проверки

\n
  1. Назовите запись. Зафиксируйте record id, версию, источник и владельца до открытия внешнего инструмента.
  2. Классифицируйте содержимое. Отделите публичное описание от персональных данных, секретов, клиентского кода и внутренних деталей. Не угадывайте класс по имени файла.
  3. Проверьте policy и договор. Найдите условие для выбранного сервиса и режима: хранение, обучение, регион, субподрядчик и срок. Если условие относится к другому плану, оно не подходит.
  4. Проверьте маршрут. Укажите разрешённый endpoint, proxy и правило egress. Наличие соединения не является разрешением на данные.
  5. Сверьте полномочия. Requester должен иметь доступ к записи. Владелец должен разрешить именно этот scope, destination и срок.
  6. Проверьте дополнительные источники контекста. Уточните, не добавляет ли режим историю, файлы репозитория, поиск или другие поля запроса.
  7. Оставьте отрицательный путь. При неизвестном поле не маскируйте данные и не переходите в другой аккаунт или интерфейс. Запишите вопрос, владельца и условие возобновления.
\n

Что нельзя считать доказательством

\n

Название инструмента, значок щита, корпоративная почта и доступ к приложению не доказывают допустимость конкретного payload. Слово «анонимизированный» тоже недостаточно: нужно знать, какие поля удалены, можно ли восстановить субъекта и кто проверил преобразование. Маскирование email не очищает токен в заголовке или идентификатор в URL.

\n

Не смешивайте учебную карточку с реальной политикой. В примере используются значения с префиксом synthetic-; они не описывают вашу систему, vendor contract или фактическое хранение. Пример показывает форму fail-closed решения. Он не даёт production-результата и не заменяет legal, security или data-owner review.

\n

Ограничения и критерий готовности

\n

Подход не отвечает за правильность самой классификации. Он делает неизвестность видимой. Если каталог данных устарел, approval выдан не тому владельцу или сетевой журнал неполон, проверка должна остановиться. Она также не доказывает, что ответ модели верен, что поставщик не изменит условия или что будущая функция инструмента сохранит прежний маршрут.

\n

Материал готов к применению в конкретной команде, когда для одного реального типа контекста можно предъявить четыре связанные записи: версионированный class, policy/contract condition для выбранного режима, проверенный egress и датированное scoped authorization. Отрицательный тест должен показать stop при истёкшем сроке, чужом scope или неизвестном endpoint. После этого команда может передавать только разрешённый минимальный фрагмент и восстановить, почему решение было принято.

\n

Проверяемые источники

\n" + "excerpt": "Практический маршрут для безопасной передачи контекста: классификация записи, условия сервиса, egress и scoped authorization с воспроизводимым fail-closed тестом.", + "contentHtml": "

Инженер открывает AI-чат, чтобы разобраться с ошибкой, и вставляет десять строк production-лога. В строках остаются email, внутренний идентификатор и фрагмент заголовка запроса. Ответ выглядит полезным, но по нему нельзя понять, какие ещё данные добавила выбранная поверхность, какой endpoint получил запрос и имел ли сотрудник право передавать именно эту запись.

\n

Здесь полезно разделить четыре независимых вопроса: что находится во фрагменте, разрешает ли policy выбранную обработку, куда технически уйдёт запрос и кто выдал полномочия на конкретный scope. «Корпоративный» аккаунт, allow-list в firewall или согласие в чате отвечает только на часть вопросов. Если любой из них не доказан, безопасный результат — остановить отправку и оставить причину для следующей проверки.

\n

Начните с записи, а не с инструмента

\n

До открытия внешнего интерфейса зафиксируйте объект передачи. У него должны быть recordId, версия, источник, владелец данных и назначенный класс. Класс описывает содержимое, а не право отправить его наружу: public, internal и restricted — это свойства данных, но не готовое разрешение для любого сервиса.

\n

Минимальная карточка должна связывать данные с решением. В ней укажите цель, requester (инициатора), назначение, версию policy, условие хранения, срок действия и ссылку на полномочие владельца. Не подменяйте неизвестный класс словом «внутренний» и не переносите разрешение с соседнего файла: другой record, режим или endpoint создаёт новое решение.

\n
\"Пошаговая
Учебный маршрут отделяет доказательства до действия от самого hand-off. Любое несовпадение возвращает safe stop; схема не выполняет сетевой запрос.
\n

Видимый prompt не равен исходящему запросу

\n

Поле чата показывает только часть контекста. Конкретный продукт может добавить историю диалога, имя репозитория, открытый файл, результаты поиска, выбранные расширения или служебные параметры. Это не универсальное свойство всех AI-инструментов: состав нужно проверять для конкретной поверхности, тарифного режима и включённых функций.

\n

Документация GitHub для Copilot Chat в GitHub прямо описывает обработку пользовательского ввода вместе с контекстом репозитория и, в некоторых случаях, результатами Bing. В той же документации указано, что ответы нужно проверять, а для чувствительного кода — просматривать и тестировать результат. Поэтому в карточке фиксируйте не только текст вопроса, но и surface, включённые источники контекста и фактическое назначение.

\n

После формирования запроса нужен второй барьер: prompt builder не должен принимать данные, пока детерминированная проверка не вернула разрешённый исход. Это не оценка качества ответа модели. Её результат может быть неверным даже при полностью разрешённом payload, поэтому проверка безопасности и проверка корректности ответа остаются разными этапами.

\n

Четыре проверки, которые нельзя слить в одну

\n

Первая проверка отвечает за содержимое: классификатор или владелец данных подтверждает, что record относится к заявленному классу после маскирования. Вторая отвечает за условия обработки: policy и договор должны относиться к выбранному продукту, режиму, региону и сроку хранения. Слово «анонимизированный» само по себе не заменяет проверку остаточных идентификаторов и возможности восстановления субъекта.

\n

Третья проверка отвечает за маршрут. Allow-list говорит, что соединение разрешено к определённому назначению, но не делает любой payload допустимым. Укажите endpoint, прокси, включённый поиск и дополнительные интеграции. Если правило разрешает домен шире, чем договорённый сервис, это повод сузить маршрут или остановить отправку.

\n

Четвёртая проверка отвечает за полномочия. Requester должен иметь доступ к record, а владелец должен разрешить цель, объём, назначение и срок. Авторизация без recordId и expiry не даёт воспроизводимого scope. Принцип zero trust NIST формулирует похожую границу: доверие не следует выводить только из сетевого положения или принадлежности устройства, а аутентификацию и авторизацию нужно выполнять для конкретного ресурса.

\n
Как читать результат pre-flight проверки
КонтрольЧто доказываетЧего не доказываетДействие при пробеле
Класс recordСодержимое отнесено к правилу с владельцем и версиейЧто внешний сервис вправе его обрабатыватьОстановить и запросить классификацию
Policy и условия сервисаДля выбранной surface описаны обработка, хранение и режимЧто запрос пошёл именно по этому маршрутуПроверить конкретный план, endpoint и дату действия
Egress allow-listСеть допускает заявленное техническое назначениеЧто payload соответствует policy и полномочиямНе отправлять данные до сверки с классом и scope
Scoped authorizationКонкретный requester получил срок и цель для recordЧто сервис не добавит другой контекстПолучить новое разрешение и проверить surface
Request traceВиден фактический outbound payload и destinationЧто ответ модели точен или безопасен для публикацииОтделить расследование передачи от ревью ответа
\n

Воспроизводимый fail-closed пример

\n

Ниже — самостоятельный пример для Node.js 20 и новее. Он работает только с объектами в памяти, не читает файлы, не вызывает модель и не делает HTTP-запрос. Входы с префиксом synthetic- намеренно учебные. Команда запуска показывает отрицательный путь: restricted-класс блокирует hand-off даже при наличии пользователя и маршрута.

\n
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, но сохраните такой же отрицательный контракт.

\n

Положительный результат тоже ограничен. Если заменить класс на synthetic-public, функция проверит только перечисленные поля. Она не доказывает реальное поведение поставщика, полноту сетевого журнала, юридическое основание обработки или отсутствие скрытого контекста в интерфейсе. Это gate перед следующим полномочным шагом, а не сертификат безопасности.

\n

Если данные уже отправили

\n

Сначала остановите повторение: отключите интеграцию или запретите дальнейший egress для этой surface. Не удаляйте единственную копию доказательств до согласования с владельцем расследования. Сохраните минимум, необходимый для анализа: время, requester, идентификатор операции, destination, режим продукта и классификацию без копирования чувствительного payload в новый тикет.

\n

Затем разделите два вопроса. Факт передачи устанавливается по журналам клиента, прокси и поставщика, если они доступны. Состав исходного payload устанавливается по журналу формирования запроса, а не по ответу модели. Если одного из журналов нет, это ограничение результата расследования, а не основание считать, что утечки не было.

\n

Порядок pre-flight для команды

\n
  1. Назовите record. Зафиксируйте recordId, версию, источник, владельца и цель. В заявке используйте ссылку или безопасный отпечаток, а не необработанный лог.
  2. Определите класс. Проверьте персональные данные, секреты, токены, клиентский код и косвенные идентификаторы после всех преобразований. Если классификация не подтверждена, результатом считается stop.
  3. Выберите конкретную surface. Запишите продукт, тарифный режим, историю, repository context, web search и расширения. Не переносите вывод из другого аккаунта или интерфейса.
  4. Сверьте условия обработки. Найдите действующие policy и договорные условия для выбранного режима: retention, обучение, регион, субподрядчики и срок. Ссылка на общий сайт продукта недостаточна.
  5. Проверьте egress. Сопоставьте endpoint, proxy и сетевое правило с разрешённым назначением. Доступный порт или удачный DNS-ответ не являются допуском payload.
  6. Проверьте полномочия. Сопоставьте requester, owner, record, purpose, destination и expiry. Устаревшее «команде можно» не заменяет scoped authorization.
  7. Проведите synthetic-тест. Для разрешённого и запрещённого fixture проверьте decision, reason и отсутствие вызова внешнего клиента на stop. Сохраните версию правил и дату прогона.
\n

Ограничения применимости

\n

Этот маршрут не классифицирует данные автоматически и не заменяет DLP, privacy review, договор с поставщиком, управление доступом или требования закона. NIST AI RMF — добровольная рамка для управления рисками AI; она помогает организовать функции govern, map, measure и manage, но не выдаёт разрешение на конкретную запись. Решение о законности обработки остаётся у вашей организации и зависит от юрисдикции и контекста.

\n

Локальная модель уменьшает внешний маршрут, но не отменяет права доступа, локальное хранение журналов и риск попадания секрета в историю. Маскирование уменьшает объём, но может оставить комбинацию полей, по которой субъект восстанавливается. Человеческое согласование полезно для спорных случаев, но оно не должно быть единственным барьером, если запросы проходят через автоматическую интеграцию.

\n

Проверка также не говорит, что ответ модели верен. Для кода нужны ревью, тесты и сканирование зависимостей; для инцидента — независимое подтверждение фактов; для решения о клиенте — отдельная процедура. Без этих этапов безопасный hand-off всё равно может привести к неверному действию.

\n

Критерий готовности

\n

Путь можно включать для конкретного типа данных, если другой инженер воспроизведёт его без устного контекста. Должны существовать версия классификации, ссылка на policy для конкретной surface, запись проверенного destination, scoped authorization и журнал решения. Негативный тест должен показывать stop при restricted-классе, чужом scope, истёкшем сроке и неизвестном endpoint.

\n

Если хотя бы одно доказательство отсутствует, не называйте результат «разрешённым». Оставьте данные локально, верните причину отказа и назначьте владельца следующего вопроса. После появления недостающего evidence повторите весь pre-flight: изменение режима, маршрута или версии record делает старое решение неприменимым.

\n

Проверяемые источники

\n" } diff --git a/editorial/agent-rewrites/100.json b/editorial/agent-rewrites/100.json index 69481d9..cad5b95 100644 --- a/editorial/agent-rewrites/100.json +++ b/editorial/agent-rewrites/100.json @@ -2,6 +2,6 @@ "index": 100, "slug": "editorial-2025-03-field-knowledge-retrieval", "title": "Когда поиск по базе знаний должен остановиться", - "excerpt": "Высокая похожесть найденного фрагмента не доказывает его актуальность, доступность и пригодность для цитирования. Разбираем stop-условие, проверку источника и безопасный путь для неполного результата.", - "contentHtml": "

Поиск по инженерной базе часто ломается не тогда, когда ничего не нашёл. Опаснее другой симптом: система возвращает убедительный фрагмент, а команда принимает его за действующее правило. Документ уже закрыт для текущего пользователя, срок его действия истёк или в нём нет точного места, которое подтверждает вывод. Поиск показывает высокий score, интерфейс показывает ответ, а ошибка обнаруживается позже.

\n

Цена такой ошибки измеряется не длиной задержки. Старая инструкция может привести к неверной миграции. Закрытый фрагмент может попасть в ответ человеку без права доступа. Неточная цитата превращает предположение в решение на ревью. Исправлять последствия дороже, чем остановить ответ на несколько минут и передать вопрос владельцу источника.

\n

Тезис: похожесть не равна доказательству

\n

Retrieval должен отвечать на два разных вопроса. Первый: насколько найденный фрагмент похож на запрос. Второй: можно ли использовать его для конкретного ответа. Векторный или текстовый поиск решает только первый вопрос. Он ранжирует результаты. Он не подтверждает дату, права и точный смысл документа.

\n

Поэтому ответ разрешается продолжить только для записи, которая одновременно проходит три проверки: источник доступен этому запросу, источник свеж по заданному правилу и в нём есть точная цитата с устойчивым anchor. Если хотя бы одно условие не выполнено у всех кандидатов, система возвращает stop. Она не заполняет пробел вероятным пересказом.

\n

Stop не означает «в базе ничего нет». Он означает «найденное нельзя безопасно использовать». Это важное различие для диагностики. Пользователь должен увидеть безопасную причину и следующий маршрут, но не закрытый текст. Владелец документа должен понять, что обновить или разрешить. Так система сохраняет и полезность, и границу доказательств.

\n

Механизм проверки

\n

У записи источника должен быть контракт. Минимальный набор полей выглядит так: стабильный идентификатор, версия, URI, anchor, владелец, класс доступа, дата публикации, дата индексации и локальная дата истечения. Поле retrievedAt фиксирует момент, когда поиск увидел запись. Эти даты нельзя сводить к одному timestamp: задержка индекса и срок действия документа описывают разные риски.

\n

Сначала система получает кандидатов. Затем применяет policy-фильтр. Проверка прав происходит до передачи excerpt в контекст ответа. Проверка свежести зависит от типа вопроса. Для операционного вопроса истёкшая инструкция непригодна. Для исторического вопроса она может быть полезна, но ответ должен назвать версию и дату. В обоих случаях правило задают метаданные и владелец политики, а не score.

\n

Положительная ветка возвращает source id, version, URI, anchor, retrievedAt и разрешённый фрагмент. Отрицательная ветка возвращает код причины: access-denied, expired, missing-anchor или unknown-policy. Закрытый excerpt не входит в отрицательный результат. Такой контракт не делает источник истинным автоматически. Он не даёт системе скрыть отсутствие доказательства.

\n
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 здесь не подключены к настоящей базе и не подтверждают реальную авторизацию. Пример показывает границу: положительный результат ещё требует ручного сравнения утверждения с источником, а отрицательный результат останавливает дальнейшую обработку.

\n

Симптомы, причины и действия

\n
Диагностика retrieval-ответа
СимптомПричинаПроверкаДействие
Высокий score, но документ старыйРанжирование не учитывает срок действияСравнить expiresAt и retrievedAtОстановить ответ и направить к владельцу документа
Найден закрытый фрагментПоиск и проверка прав разделены или проверка выполнена поздноПроверить access decision до передачи excerptВернуть безопасную причину без текста источника
Цитата ведёт на страницу без местаВ индексе сохранён URI, но нет устойчивого anchorОткрыть URI и проверить заголовок, номер раздела или якорьОстановить ответ и исправить карточку источника
Разные даты в индексе и документеНе различены публикация, индексация и срок действияСопоставить все даты и владельца каждойИсправить policy или обновить индекс
Система отвечает «ничего нет»Reject reasons потерялись после фильтраПроверить structured result и журнал решенияПоказать safe reason и следующий маршрут
\n

Почему HTTP-дата не решает задачу свежести

\n

HTTP-заголовок Last-Modified сообщает дату, когда origin считает выбранное представление изменённым. Это полезный сигнал для индексации и условных запросов. Но он не доказывает, что инструкция всё ещё действует. Правило могло устареть из-за миграции, смены владельца или изменения контекста, даже если файл с тех пор не менялся.

\n

Практическая политика должна назвать, какая дата управляет ответом. Например, publishedAt отвечает за версию документа, indexedAt показывает задержку конвейера, а expiresAt задаёт допустимый срок для операционных вопросов. Если источник не предоставляет нужные данные, это не повод угадывать. Нужна остановка или явное решение владельца о том, какое ограничение действует.

\n

Права доступа проверяются отдельно

\n

Доступ к документу нельзя выводить из факта, что поисковый сервис смог его прочитать. Индекс может работать с расширенными правами, а запрос пользователя — с ограниченными. Проверка должна учитывать субъекта, ресурс и цель обращения. Deny должен произойти до того, как закрытый фрагмент попадёт в контекст следующего шага.

\n

Безопасный stop-ответ содержит только то, что разрешено policy: например, внутренний идентификатор записи, общий код причины и группу владельца. Не следует возвращать закрытый заголовок, соседние предложения или признаки, по которым можно восстановить содержание. Уровень подробности диагностического сообщения — тоже часть модели доступа.

\n
\"Схема
Остановка происходит внутри цикла: кандидат проходит проверку доступа, свежести и anchor до формирования ответа.
\n

Порядок действий для одной базы

\n
  1. Разделите вопросы на операционные и исторические. Зафиксируйте разные правила свежести, если они действительно различаются.
  2. Опишите карточку источника. Сохраните id, версию, URI, anchor, владельца, класс доступа и нужные даты.
  3. Определите обязательные условия ответа: доступ, свежесть и точная цитата. Назовите stop-коды для каждого нарушения.
  4. Проверьте отрицательный путь. Подложите кандидата с высоким score, но с истёкшим сроком или запрещённым доступом.
  5. Убедитесь, что excerpt закрытого кандидата не покидает слой проверки. В результате должны остаться только разрешённая причина и маршрут.
  6. Проверьте положительный путь вручную. Откройте exact source, сравните claim с anchor, подтвердите версию и сохраните citation.
  7. Назначьте владельца остаточного риска. Если policy неизвестна, вопрос должен попасть к владельцу policy, а не к слою ответа.
\n

Отрицательный путь важнее красивого ответа

\n

Качество retrieval видно не только по найденным документам. Оно видно по тому, как система ведёт себя при конфликте. Если документ похож на запрос, но истёк, она должна сохранить причину и остановиться. Если фрагмент разрешён, но anchor отсутствует, она не должна ссылаться на всю страницу. Если policy неизвестна, она не должна превращать неизвестность в allow.

\n

Простой тест проверяет именно это поведение: highest-score candidate получает значение expired или access-denied; итоговый объект имеет статус stop; поля claim и citation.excerpt отсутствуют; присутствуют причина и действие владельца. Это проверяет контракт учебного модуля, но не доказывает работу вашей production-базы. Для реальной системы нужны отдельные проверки identity, источника и наблюдаемости.

\n

Ограничения

\n

Эта модель не выбирает лучший алгоритм поиска и не обещает, что keyword search, embeddings или reranking дадут конкретную точность. Она не заменяет классификацию документов, аудит прав и управление версиями. Score остаётся полезным для порядка кандидатов, но не становится доказательством. На качество ответа также влияет полнота корпуса: если нужного документа нет, фильтр не создаст его.

\n

Учебный код не читает настоящую wiki, не обращается к сети и не выполняет реальную authorization decision. Все записи в примере условны. Нельзя переносить его значения дат, score или статусы в production. Переносить стоит только форму результата: accepted либо stop, структурированную причину, citation с anchor и явного владельца следующего действия.

\n

Проверяемый критерий готовности

\n

Работа готова, когда для одного выбранного типа вопроса система может показать полный путь. Разрешённый свежий источник даёт версию, URI, anchor и дату retrieval, после чего человек подтверждает claim. Просроченный, закрытый или нецитируемый кандидат даёт stop без раскрытия запрещённого текста. В обоих случаях есть воспроизводимая проверка и назначенный владелец.

\n

Если тест с высоким score и истёкшим документом всё ещё формирует ответ, проблема находится не в формулировке prompt. Не хватает контракта между индексом, policy и слоем ответа. Сначала добавьте metadata и отрицательную ветку. Только после этого имеет смысл настраивать ranking.

\n

Проверяемые источники

\n" + "excerpt": "Похожий фрагмент не становится доказательством только из-за высокого score. Разбираем проверку версии, доступа и точной цитаты, а также воспроизводимый stop-ответ для неполного результата.", + "contentHtml": "

Представим рабочий вопрос: «Как откатить релиз?» Поиск возвращает абзац из runbook с высоким score. В нём есть знакомые слова, но документ уже заменён, фрагмент закрыт для текущей роли или ссылка ведёт только на главную страницу. Если сразу передать текст в ответ, система превратит совпадение слов в указание к действию.

\n

Здесь опасен не сам поиск. Ошибка возникает на границе между найденным кандидатом и разрешённым утверждением. Старый runbook может привести к неправильной последовательности отката. Закрытый фрагмент нельзя раскрывать даже ради «полезного контекста». Ссылка без точного места не позволяет коллеге перепроверить вывод. Поэтому зрелый retrieval должен уметь завершаться ответом stop, когда доказательств недостаточно.

\n

Похожесть отвечает только на один вопрос

\n

Retrieval — этап, который выбирает фрагменты из корпуса по запросу. Текстовый индекс, embeddings и reranker помогают упорядочить кандидатов. Их score отвечает на вопрос «насколько запись похожа на запрос по данной модели». Он не отвечает на вопросы «действует ли правило», «можно ли его показать этой роли» и «есть ли в источнике точное место для цитирования».

\n

Эти вопросы нужно разделить в контракте. Пусть кандидат проходит три независимые проверки: доступ разрешён субъекту запроса, срок пригодности не истёк по политике конкретного типа вопроса, а источник содержит устойчивый anchor — заголовок, номер раздела или другой способ указать точное место. Только после этого разрешённый фрагмент можно передавать в слой формирования ответа.

\n

Если лучший по score кандидат не прошёл проверку, это ещё не означает немедленный stop: можно проверить следующий кандидат. Но если все подходящие записи отклонены, система обязана вернуть безопасную причину и маршрут к владельцу. Она не должна «додумывать» недостающий фрагмент из похожих документов.

\n

Карточка источника и границы дат

\n

Поиск не сможет проверить то, чего нет в метаданных. Для каждой записи полезно хранить стабильный sourceId, version, канонический uri, anchor, класс доступа, владельца, publishedAt, indexedAt и управляемый политикой expiresAt. Это не универсальная схема базы данных, а минимальный набор для рассматриваемого решения. Поля надо согласовать с владельцем корпуса и правилами доступа.

\n

Даты имеют разные смыслы. publishedAt описывает версию, indexedAt показывает задержку индексатора, а expiresAt — локальное правило, после которого операционный вопрос нельзя закрывать этой записью без подтверждения. Для исторического вопроса срок может быть другим: старый документ допустим как описание прошлого, но ответ должен назвать версию и дату.

\n

Не подменяйте это правило заголовком HTTP. RFC 9110 определяет Last-Modified как время, когда origin, по собственному мнению, в последний раз изменил выбранное представление. Заголовок полезен для условных запросов и кэша, но не говорит, отменено ли содержание документа, изменился ли владелец или разрешён ли доступ. Семантический срок действия остаётся частью политики корпуса.

\n
Цикл проверки ответа по базе знаний: запрос, кандидаты, проверка доступа и срока, точный якорь, сверка человеком, ответ или остановка
Кандидат становится частью ответа только после проверки прав, срока и точного места в источнике; любая невыполненная проверка ведёт в безопасную ветку остановки.
\n

Доступ проверяется до передачи фрагмента

\n

Индексатор может читать больше, чем конкретный пользователь. Поэтому факт, что поисковый сервис нашёл запись, ничего не доказывает о праве показать её субъекту запроса. Сначала определите субъекта, ресурс и цель обращения, затем получите решение authorization. Только после положительного решения можно раскрывать excerpt.

\n

Безопасный stop-ответ не пересказывает запрещённый текст. В нём достаточно общего кода access-denied, expired, missing-anchor или policy-unknown и следующего действия: запросить доступ, назначить владельца, обновить карточку или выбрать другой корпус. Даже название закрытого документа может быть чувствительным, поэтому его возврат тоже должен проходить через policy.

\n

Это согласуется с моделью Zero Trust из NIST SP 800-207: доверие не выводится из нахождения в локальной сети или из принадлежности ресурса организации, а authentication и authorization рассматриваются как разные функции до установления сессии к ресурсу. Стандарт не описывает ваш индекс и не выдаёт готовую ACL; он задаёт границу, которую нельзя заменять сетевой близостью.

\n

Воспроизводимый пример с положительной и отрицательной веткой

\n

Ниже — самостоятельный пример для Node.js 18+ без внешних пакетов. Данные синтетические, дата намеренно зафиксирована, чтобы результат не менялся завтра. Сохраните фрагмент как retrieval-check.mjs или вставьте его в оболочку node --input-type=module. Ожидаемый вывод: сначала разрешён текущий документ с меньшим score, затем stop после искусственного истечения всех записей.

\n
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 здесь передаётся вручную и не является решением доверенного сервиса.

\n

Диагностика симптома по слоям

\n
Что проверять, если ответ выглядит убедительно
СимптомЧто он доказываетЧто проверитьСледующий шаг
Высокий score у старого документаКандидат похож на запрос по модели ранжированияexpiresAt, тип вопроса, владелец политикиПроверить следующий кандидат или вернуть expired
Индекс возвращает закрытый excerptИндексатор видит записьРешение доступа до формирования контекстаУбрать текст из результата и вернуть безопасный код
URI открывается, но место не найденоСсылка существуетВерсию и устойчивый заголовок или fragmentОстановить цитирование и исправить карточку
Документ не менялся годПредставление долго не изменялось по данным originОтменённые правила, владельца и семантический срокНе считать Last-Modified доказательством актуальности
Ответ говорит «ничего нет»После фильтров не осталось разрешённого кандидатаСохранённые reject-коды и границу раскрытияПоказать причину и адресата, не скрывая следующий шаг
\n

Негативный путь должен быть наблюдаемым

\n

Stop полезен только тогда, когда его можно отличить от пустого индекса и от сбоя самого retrieval. Внутри системы сохраняйте request id, тип запроса, количество кандидатов, причины отклонения и версию policy. Отдельно измеряйте долю остановок по причинам. Не записывайте в диагностический журнал закрытый excerpt или чувствительные названия без разрешения.

\n

Разделяйте три результата: no-match — подходящих кандидатов не найдено; stop — кандидаты были, но ни один нельзя использовать; system-error — проверка не завершилась из-за сбоя. Пользовательский интерфейс может показывать один безопасный текст, но внутренний контракт должен сохранять различие. Иначе команда начнёт чинить ranking, когда сломана policy, или повторять запрос при отказе авторизации.

\n

Проверяйте и положительную ветку вручную. Откройте exact source, сравните формулировку утверждения с указанным anchor, убедитесь в версии и проверьте, что право относится к тому же субъекту и ресурсу. Высокий score, зелёный health-check или наличие ссылки не заменяют эту сверку.

\n

Порядок внедрения для одной базы

\n
  1. Разделите запросы на операционные, справочные и исторические. Для каждого типа назначьте владельца и правило свежести.
  2. Опишите карточку источника: id, версия, URI, anchor, access class, владелец и раздельные даты публикации, индексации и истечения.
  3. Определите порядок: сначала policy доступа, затем свежесть, затем наличие точной цитаты. Не передавайте excerpt до завершения этих проверок.
  4. Назначьте стабильные stop-коды и безопасный текст для пользователя. Закрытый фрагмент не должен попадать ни в ответ, ни в обычный журнал.
  5. Соберите фиксированный набор тестов: свежий разрешённый кандидат, просроченный кандидат с высоким score, запрещённый кандидат, ссылка без anchor и пустой индекс.
  6. Запустите отрицательные тесты на каждом релизе индексатора. Проверяйте не только статус, но и отсутствие полей excerpt и citation в stop-ответе.
  7. Для разрешённого результата сохраните citation с версией и anchor. Человек должен суметь открыть источник и восстановить границу утверждения.
  8. Наблюдайте причины остановок и регулярно возвращайте карточки с истёкшим сроком владельцам корпуса. Ranking настраивайте только после исправления контракта данных.
\n

Что этот подход не доказывает

\n

Проверка доступа, срока и anchor не делает документ истинным. Она не оценивает полноту корпуса, качество формулировки, причинность рекомендации или пригодность бизнес-решения. Утверждение может быть точно процитировано и всё равно оказаться неверным из-за ошибки самого источника. Поэтому для рискованных операций нужен отдельный review владельца процесса.

\n

Score нельзя сравнивать между разными индексами и моделями без отдельной калибровки. Значение 0.99 в учебном примере не является универсальным порогом. Текстовый поиск, embeddings и reranking имеют разные свойства; выбор алгоритма остаётся инженерным экспериментом на вашем корпусе и eval-наборе.

\n

Локальное поле expiresAt не является стандартным HTTP-заголовком и не появляется автоматически из Last-Modified. Срок должен поддерживаться процессом владельца. NIST SP 800-207 не заменяет вашу матрицу ролей, а RFC 9110 не определяет жизненный цикл wiki-документа. Эти границы нужно оставить видимыми, иначе учебный контракт начнут выдавать за готовую платформу.

\n

Критерий готовности

\n

Маршрут готов к использованию, если независимый проверяющий может восстановить запрос, policy и выбранную версию источника. Для свежей разрешённой записи ответ содержит точный URI и anchor, а утверждение совпадает с открытым фрагментом. Для просроченной, запрещённой или нецитируемой записи результатом становится stop без раскрытия текста и с понятным следующим действием.

\n

Начинайте с одного типа операционных вопросов и пяти негативных fixtures из списка выше. Если система не проходит этот малый набор, добавление ещё одного reranker только увеличит число убедительных, но непроверяемых ответов. Правильный stop — не провал поиска, а честная граница его доказательной силы.

\n

Проверяемые источники

" }