Production readiness: ризик, дані та право на дію

Гарний patch не дає агенту права читати customer data, тримати production token чи застосовувати migration. Production readiness починається з межі authority, а не з фінального схвалення. На одному change сервісу CartRelay пройдемо шлях від невинного тікета до production rollout і подивимось, де саме закінчується свобода агента:

Сьогодні пройдемо: рівень 23. Працюємо на одному change CartRelay; справжні production credentials і customer records не використовуються.

Ризик до першого prompt

Тікет Fix duplicate refund webhook звучить як звичайний bugfix: знайти причину, виправити handler, закрити. Але доки не відкриєте логи, ви не знаєте чотирьох речей:

Тому перше інженерне питання - не "як лагодити", а "що agent зможе зробити, якщо помилиться або витлумачить завдання ширше, ніж мав на увазі автор". Відповідь і є міткою ризику - і призначають її до prompt, бо сам prompt уже відкриває дані та tools.

Класифікація ризику обмежує не глибину міркування Claude, а його максимальну реальну владу. Думати модель може про будь-що - дотягнутися має тільки до дозволеного.

"Попросимо спочатку подивитися" - це вже рішення про доступ до даних і tools.

Швидка класифікація ризику

Швидка класифікація займає п'ять секунд і не потребує комітету: дивитесь, чого стосується завдання, і берете стартовий клас. Сумніваєтесь - мінімум review-required, і працюєте далі.

Швидкий класСтартове припущенняЩо негайно підвищує ризик
low-riskdocs, коментар, локальний renamesensitive data, external publish, shared config
review-requiredcode, config, dependency, APIbroad write, privileged credential, weak rollback
high-riskproduction, DB, secrets, moneyклас уже максимальний

Тип файлу задає нижню межу, але не вирок. Будь-яка з чотирьох осей піднімає клас сама по собі:

  1. data - public fixture чи customer record;
  2. authority - read repo чи write production DB;
  3. blast radius - один test object чи всі активні refunds;
  4. reversibility - git revert чи вже надіслана виплата.

README з production token усередині - це high-risk, яким би невинним не виглядав diff. І клас не зафіксований назавжди: правило reclassify on discovery переперевіряє його щоразу, коли розслідування знаходить новий факт.

Змінюється target, credential, data class чи side effect - старий envelope закривається.

Capability envelope

Мітка review-required сама по собі нічого не забороняє. Працювати починає картка, де ризик перекладено в конкретні дієслова і targets, - capability envelope (рамка дозволених дій). "May help with refunds" не перевірити; edit refund handler - можна.

risk: review-required
may:
  - read application code
  - edit refund handler
  - run local tests on synthetic fixtures
must_ask:
  - fetch external incident links
must_not:
  - read customer exports
  - use production credentials
  - write shared infrastructure
stop_when:
  - fix requires data backfill or external refund action

До envelope входять не лише tools, а й environment з data class. А stop_when робить discovery безпечним: нова інформація не перетворюється автоматично на новий дозвіл - вона закриває стару картку.

Зверніть увагу, як межа змінює стратегію агента: замість "виконай cleanup" він готує план, fixture і verification evidence, бо пряма дія йому не дозволена.

Для low-risk завдання envelope - це чинна політика проєкту, а не новий файл на кожен тікет. Явну картку пишуть, коли клас вищий за low або завдання виходить зі звичної зони.

CartRelay: зміна класу ризику

План був скромним: відтворити дубль, виправити idempotency check. Ось де він ламається:

1. Reproduce duplicate event on synthetic fixture        OK
2. Patch idempotency check and add regression test       OK
3. Inspect incident note: 312 affected refunds           NEW FACT
4. Suggested cleanup touches production ledger           STOP
5. Reclassify review-required -> high-risk               REQUIRED

На третьому кроці початкова класифікація вже не відповідала завданню: в incident note знайшлися 312 зачеплених refunds, а запропонований cleanup пише в production ledger.

flowchart LR I["Investigation"] --> N["New fact: production ledger"] N --> S["Old envelope closes"] S --> F["Code fix: same zone"] S --> C["Cleanup: new work item and approval"]

Схема показує розріз: новий факт закриває старий envelope, і далі живуть дві гілки з різною authority:

Reclassification не відкочує корисну роботу - вона розділяє завдання на зони з різними authority boundaries. і жодного доступу "ще на хвилинку, тільки перевірити здогадку в production".

Scope виріс, а permissions лишилися тими самими - отже, policy вже не відповідає завданню.

Три шари захисту

Перша реакція на зрослий ризик - дописати в CLAUDE.md сувору інструкцію "не чіпай production". Інструкція корисна, але це прохання, а не замок. Шари захисту відповідають на різні питання:

ШарЩо робитьЧого не гарантує
CLAUDE.md, rules, promptпояснює бажану поведінкуне блокує process чи network
permission rules, hooks, modesвирішує, чи можна викликати toolне завжди обмежує внутрішній effect процесу
sandbox, OS, network proxy, downstream IAMобмежує досяжний effectне розуміє business intent

Середній шар теж не абсолютний. auto mode знижує втому від підтверджень, але classifier лишається ймовірнісним. Ручні prompts не кращі: коли підтверджуєте майже кожен запит, pop-up перетворюється на звичку. Для критичної інфраструктури судження моделі - додатковий signal, а не enforcement.

Автономію безпечніше підвищувати, зменшуючи capability, а не посилюючи формулювання prompt.


Командні deny rules

Особистої акуратності не вистачає на другу сесію та другого розробника. Командна політика живе в .claude/settings.json проєкту й починається з того, що заборонено всім - включно з subagents:

{
  "permissions": {
    "deny": [
      "Read(./.env)",
      "Read(./secrets/**)",
      "Edit(./infra/prod/**)",
      "Bash(kubectl *)",
      "Bash(terraform apply *)"
    ],
    "ask": [
      "Edit(./migrations/**)"
    ]
  }
}
flowchart LR T["Tool call"] --> D{"deny match?"} D -->|yes| B["Blocked"] D -->|no| A{"ask match?"} A -->|yes| P["Prompt"] A -->|no| L["Allow or default flow"]

Порядок оцінки на схемі і є deny precedence: deny перевіряється раніше за ask і allow, тому ширший allow його не скасовує.

Тест простий: policy має пережити нову сесію, іншого розробника та іншого subagent. Усе, що живе тільки в голові або в одному prompt, цей тест не проходить.

JSON вище - проєктний фрагмент, а не універсальна policy для будь-якої інфраструктури.

Середовища ізоляції

Окрема тека не забирає у процесу ваші ключі: ізоляція файлів та ізоляція authority - різні речі. Питати треба не "де я працюю", а "що процес усе ще може зробити назовні". Кожне середовище має на це свою чесну відповідь:

СередовищеДобре обмежуєНе обмежує саме
branchісторію changeprocess, credentials, network
worktreeробочі файли паралельних завданьhome directory, inherited env, external APIs
Claude Bash sandboxBash filesystem і outbound domainsbuilt-in tools без permission rules
containerdependencies і process treemounted sockets, injected secrets, unrestricted egress
VM чи cloud sandboxhost і sessionprovider data flow, overbroad identity, allowed outbound
stagingвплив на production trafficshared state: чужі перевірки, спільні дані, ціна помилки не нульова

Container із mounted Docker socket чи cloud admin token зберігає великий blast radius - назва технології сама по собі нічого не ізолює. Вибір середовища починається з failure mode, а не з імені інструмента.

Два сусідні поняття сюди не входять: dry-run показує план дій, але не ізолює помилку виконання, а staging перевіряє інтеграцію після локальної пісочниці, а не замість неї.


Claude Bash sandbox

Вбудований sandbox - це OS-межа навколо Bash і його child processes: filesystem і network. Read, Edit, WebFetch і MCP живуть поза цією межею - їх закривають permission rules. Config нижче ховає ~/.aws лише від sandboxed Bash: для built-in tools потрібне відповідне правило Read deny у permissions.

{
  "sandbox": {
    "enabled": true,
    "failIfUnavailable": true,
    "allowUnsandboxedCommands": false,
    "filesystem": {
      "denyRead": ["~/.aws", "~/.ssh"]
    },
    "network": {
      "allowedDomains": ["registry.npmjs.org"]
    },
    "credentials": {
      "envVars": [
        { "name": "GITHUB_TOKEN", "mode": "deny" }
      ]
    }
  }
}
--dangerously-skip-permissions не створює sandbox.

Витік із трьох частин

Щоб украсти private data, агенту не треба писати у файл і ламати permission. Досить, щоб в одній сесії зійшлися три речі, кожна з яких окремо виглядає невинно.

flowchart LR P["Private data"] --> X["Exfiltration path"] U["Untrusted content"] --> X E["Outbound channel"] --> X

На схемі три входи одного exfiltration path: private data в контексті, untrusted content, який керує поведінкою агента, і відкритий outbound channel. У звичайній робочій сесії всі три непомітно поєднуються:

Захист - розірвати будь-яку ланку цього ланцюга:

  1. browsing task не отримує private data;
  2. sensitive task не читає untrusted content;
  3. task з обома входами позбавляється arbitrary egress.

Allowlist звужує поверхню, але довірений API теж приймає довільні дані - для чутливого workflow потрібен scoped operation чи credential proxy. Prompt injection стає production risk лише через доступний ланцюг capability.


Дані локальної сесії

CLI і tools виконуються на вашій машині, але дані розмови з моделлю передаються мережею: prompts, вибраний контекст і model outputs потрапляють до provider. Локальний subagent відокремлює контекст і tools, а не гарантує data residency.

flowchart LR W["Workspace"] --> C["Selected context"] T["Tool results and subagent summary"] --> C C --> M["Model provider"] M --> O["Model output"] W --> L["Local transcript, logs, screenshots"]

На схемі два маршрути: верхнім маршрутом контекст потрапляє до provider, а звідти повертається output; нижній лишає локальні артефакти сесії. Перед чутливим розслідуванням варто зрозуміти обидва:

"Файл не покинув ноутбук" не означає, що його вміст не потрапив у model context - і водночас у локальний transcript.


Sanitization

Розслідуванню потрібні production-деталі, а контексту вони протипоказані. Вихід - fixture, з якого identity зникла, а причинний signal лишився. Raw export у Claude session не входить: у fixture його перетворює людина або deterministic redactor до сесії, Claude бачить лише результат. Порівняйте вхід і вихід:

{
  "email": "customer@example.test",
  "authorization": "Bearer live_token_redacted",
  "event_id": "evt-real-8841",
  "order_id": "order-real-771",
  "amount_cents": 4999,
  "retry_after_ms": 812
}
{
  "customer_id": "CUST-17",
  "authorization": "<removed>",
  "event_id": "EVT-DUP-1",
  "order_id": "ORDER-42",
  "amount_cents": 4999,
  "retry_after_ms": 812
}

Прибрали email, bearer token і реальні ідентифікатори. Лишилися duplicate event key, зв'язки між записами, amount class, порядок і timing - тому bug відтворюється:

Гарна sanitization - це transformation contract, а не ручний пошук очевидних email: контракт називає властивості даних, які зобов'язані вижити.

Contract і друга пара очей потрібні, коли fixture іде в репозиторій або несе грошові пороги. Для одноразової локальної розвідки досить вручну замаскувати ідентифікатори за тими самими правилами.

Межа credentials

Fix готовий, і настає момент, заради якого будувалася вся межа: хто тримає production credential? Відповідь коротка - не agent. Інакше будь-яка помилка, dependency script чи prompt injection успадкує production authority цілком.

flowchart LR C["Claude prepares intent"] --> R["Independent review"] R --> J["Protected job"] J --> I["Short lived identity"] I --> T["Exact production target"]

Authority на схемі з'являється поетапно: Claude готує intent, незалежний reviewer схвалює, і лише після цього protected job отримує short-lived identity для exact target.

Approval керує моментом появи authority, а не лише моментом запуску script.

Якщо credential можна скопіювати з agent environment і використати поза job - boundary завелика.

Реакція на витік секрету

Токен майнув у виводі команди - і рефлекс вимагає стерти рядок, почистити чат, переписати історію. Рефлекс помиляється: жодна з цих дій не відкликає credential. Порядок інший:

1. Stop session and affected job
2. Revoke or rotate credential
3. Inspect audit log and possible use
4. Replace dependent configuration safely
5. Remove secret from logs, transcripts and repository
6. Run secret scan and record follow-up

Витік секрету - це інцидент, навіть коли репозиторій private. Різниця лише в імовірності, а не в процедурі.


Deployment intent

Фінальний крок - не "Claude, задеплой". Claude пише deployment-intent.yaml - точний, перевірюваний документ про те, що і куди поїде. Право виконати change залишається за захищеним pipeline:

service: cartrelay-refunds
environment: production-eu1
artifact: sha256:7f4d-example
change: idempotency guard for duplicate webhook
database_write: false
forbidden:
  - refund replay
  - ledger cleanup
verification:
  - duplicate_event_rate
  - refund_mismatch_rate
rollback: previous artifact digest
УчасникРішення
Claudepatch, tests, intent, expected signal, rollback draft
Reviewerчи узгоджується exact change із business risk
Protected pipelineexact artifact, target, policy, short-lived identity
Runtime controlcanary, telemetry, abort, rollback

Approval стосується digest і target, а не вільного тексту "deploy latest". Production DB cleanup лишається окремою high-risk операцією і не ховається всередині deploy. Людина не копіює команди в prod shell - вона видає дозвіл обмеженій automation.

Production boundary має виконуватися системою, яка не залежить від слухняності Claude.


Canary та умови зупинки

Reviewer перевіряє intent до side effect. Що відбувається після - питання вже не до reviewer, а до runtime:

canary_percent: 5
observe_minutes: 10
continue_if:
  duplicate_event_rate: decreasing
  refund_mismatch_rate: unchanged
abort_if:
  error_rate_percent: "> 1"
  ledger_invariant_failures: "> 0"
stateDiagram-v2 [*] --> Canary: approval Canary --> Rollout: signal ok Canary --> Abort: abort condition Abort --> Rollback Rollout --> [*] Rollback --> [*]

Життєвий цикл rollout на схемі: approval лише відкриває canary, а далі вирішує runtime signal - повний rollout чи abort з поверненням попереднього artifact.

Human approval і runtime automation закривають різні типи невизначеності: перший відповідає "чи варто пробувати", друга - "чи вийшло".

Якщо в команді deploy - це кнопка в CI без canary, межа лишається тією самою: Claude готує intent, кнопку натискає не він. Canary і short-lived identity - щабель зрілості pipeline, а не вхідний поріг самої межі.

Картка права на дію

Change закритий. Уся його межа вміщується на одній картці: що агенту було можна, чого не можна, що доведено і що чесно лишилося недоведеним.

risk: high
agent_may:
  - edit repository
  - test sanitized fixture
  - prepare deployment intent
agent_must_not:
  - read customer export
  - hold production credential
  - write production database
evidence:
  - regression test
  - sanitized fixture review
  - exact artifact digest
  - independent deployment approval
  - canary result
not_proven:
  - absence of every hidden defect
  - safety of a future refund backfill
owner: cartrelay-release-operator

Capability, data, environment і deployment target узгоджені з одним change. Секція not_proven - не слабкість, а чесність: canary не доводить відсутність усіх прихованих дефектів, а безпека майбутнього refund backfill - окреме рішення з окремою межею.

Наступний рівень перенесе цю картку з рівня одного change на рівень team policy, traceability і engineering culture.

Production-ready agent не отримує максимальну свободу. Він отримує мінімальну достатню authority всередині перевірюваної межі.