---
id: 6
title: "A review gate for infrastructure change"
version: "1.0.0"
status: draft
ai_assisted: true
date: 2026-08-23
authors:
  - "Theo Zourzouvillys"
tags: [infra, aws, llm, security, ci]
summary: "How to run an infrastructure plan, hold it, and apply exactly what was approved: the plan artifact as the unit of approval, an allow-list projection of the change, deterministic guardrails deciding eligibility, and an advisory reviewer that can only veto."
use_when: "One party proposes an infrastructure change and another decides whether it applies, through a pipeline or an approval step, and applying something other than what was reviewed would be a serious incident."
not_for:
  - "Deciding what your infrastructure should contain; this governs how a proposed change is reviewed and applied, not what it declares."
  - "Configuration management against running hosts, where there is no durable plan artifact to hold between decision and execution."
  - "A single operator applying changes from a laptop, where the operator is the review gate and there is no second party."
requires: []
provides:
  - "infrastructure-review-gate"
  - "plan-artifact-integrity"
  - "machine-assisted-approval"
implements_notes: [4, 9, 35, 42, 55, 64]
supersedes: null
superseded_by: null
aliases: []
crossrefs:
  ZFN-4: "Why the gate must not become a dependency of incident response: the mechanism that applies a fix cannot require the machinery most likely to be broken during one."
  ZFN-9: "The position behind the credential requirements here. Execution identity is federated and short-lived, so a stored key is never the thing an attacker takes."
  ZFN-35: "Why a workspace holds a reference to a secret rather than its value, and why dereferencing happens inside the execution boundary rather than in the control plane."
  ZFN-42: "The choice of declarative provisioning tool this specification assumes, and the reason plans are a machine-readable artifact at all rather than a rendered diff."
  ZFN-55: "Behind the isolation requirements: the execution identity per workspace exists so that a compromised control plane is not a compromised estate."
  ZFN-64: "Why an infrastructure plan is treated as a deploy — versioned, reviewable, and revertible — rather than as an administrative action outside the release process."
  ZBP-2: "Named as one way to obtain the short-lived execution credentials this document requires, rather than as a dependency. Where that blueprint specifies how a workload obtains credentials, this one specifies only what execution may hold."
references:
  - id: rfc2119
    title: "RFC 2119 — Key words for use in RFCs to Indicate Requirement Levels"
    url: https://www.rfc-editor.org/rfc/rfc2119.html
    abstract: "The source of MUST, SHOULD and MAY as terms of art with defined weight. Read together with RFC 8174, which narrows the interpretation to the capitalised forms only, so that ordinary prose using the same words carries no normative force."
  - id: tofu-json
    title: "OpenTofu — JSON output format"
    url: https://opentofu.org/docs/internals/json-format/
    abstract: "The machine-readable representation of a plan, and the schema this specification projects from. Documents the change representation, the before_sensitive and after_sensitive masks that shadow before and after, and the top-level variables property that carries every input value supplied to the plan."
  - id: owasp-llm01
    title: "OWASP Top 10 for LLM Applications — LLM01: Prompt Injection"
    url: https://genai.owasp.org/llmrisk/llm01-prompt-injection/
    abstract: "The classification for an attacker steering a model through content it was asked to process rather than through its instructions. The reason a reviewer in this design is structurally unable to widen an approval: instruction-level defences are probabilistic, and the containing architecture is not."
  - id: cwe-269
    title: "CWE-269 — Improper Privilege Management"
    url: https://cwe.mitre.org/data/definitions/269.html
    abstract: "The weakness class covering a component that can grant itself, or pass on, more privilege than it should hold. The relevant shape here is a control plane permitted to delegate an unbounded set of execution identities, which makes the isolation boundary between control plane and estate decorative."
---

## TL;DR

This specifies how a proposed infrastructure change travels from "someone asked for it" to "it
happened", when the decision to apply belongs to somebody other than the person who proposed it. The
shape of the answer: **produce a durable plan artifact, treat that artifact as the unit of approval,
apply it without re-deciding, and gate it behind deterministic rules that a language model may narrow
but never widen.**

Two properties carry the design, and both are structural rather than behavioural. The first is that
the thing approved and the thing executed are the same bytes, which turns version pinning and
staleness detection from hygiene into correctness. The second is ordering: deterministic rules decide
what is *eligible* for automatic approval, and only then does an advisory reviewer get to object. A
reviewer that could grant approval would be an instruction-following component holding the authority
of the execution identity, reading text an attacker can write.

## Applicability

Use this when a change to infrastructure is proposed through one path and applied through another —
a pipeline, a change-approval step, a service that holds plans for a team — and where applying
something other than what was reviewed would be a serious incident. It assumes a declarative
provisioning tool that can emit a plan as a durable artifact and later execute that artifact rather
than recomputing it. It is written for an estate where most proposed changes are unremarkable and a
minority are dangerous, and where the cost of reviewing every one by hand is why changes queue.

Do not use this where the proposer and the approver are the same person with the same credentials at
the same terminal: the gate then costs latency and buys nothing. Do not use it for imperative
configuration management, where nothing corresponds to a plan artifact and the requirements below
about applying what was approved cannot be met. Do not reach for the reviewer at all until the
deterministic layer exists, because the reviewer is specified here only as a veto inside an envelope
that layer defines, and without it there is no envelope.

## Scope and non-goals

In scope: the lifecycle of a run from proposal to terminal state; the integrity of the plan artifact
between decision and execution; the identity under which execution happens; the projection of a plan
that may cross a trust boundary; the deterministic eligibility rules; the reviewer contract and its
failure modes; approval, identity and separation of duties; and the audit record.

Out of scope, deliberately: what your infrastructure should declare, which is a design question this
document has no opinion on; the provisioning tool's own semantics, its state format, and how it
computes a diff; the authoring of the policy a reviewer reads, which is workspace-specific prose; and
the issuance of credentials to workloads — ZBP-2 specifies one such mechanism, but this document
requires only that execution credentials be short-lived, however they are obtained.

Also out of scope: making the gate fast. Every requirement here is chosen for correctness under
adversarial input first. Where a requirement costs latency, that is a stated price, not an oversight.

## Vocabulary

**Workspace** — a named unit of managed infrastructure with exactly one state, one execution
identity, and one policy. The unit of concurrency and of authorization.

**Configuration** — the declarative source for a workspace, captured as an immutable, content-addressed
bundle at the moment a run is proposed.

**Run** — one traversal from proposal to a terminal outcome. A run is *plan-only* (speculative, never
applicable) or *applicable*.

**Plan artifact** — the durable, machine-executable output of the plan phase, which the apply phase
consumes verbatim. Distinct from any rendering of it.

**Plan document** — the machine-readable description of a plan, from which projections are derived.
Not the plan artifact, and not executable.

**Projection** — a derived, allow-listed summary of a plan document, and the only representation of a
change permitted to cross a trust boundary.

**Guardrails** — deterministic, offline rules over a plan document that decide whether a run is
*eligible* for automatic approval.

**Eligible** — a property of a run, decided solely by guardrails, meaning automatic approval is
permitted to be considered. Not itself an approval.

**Reviewer** — an advisory component that inspects a projection and returns a verdict. In practice a
language model, but the requirements bind any implementation.

**Verdict** — a reviewer's structured output: either *approve* or *escalate*, with findings.

**Escalate** — route to human approval. The outcome of every reviewer failure.

**Control plane** — the components that orchestrate runs, hold the audit record, and serve the API.
Distinct from the execution environment and from the estate.

**Estate** — the infrastructure a workspace manages.

The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY are to be interpreted as in RFC 2119 and
RFC 8174.

## Architecture

Four boundaries do the work, and the requirements below are largely about what may cross each one.

```
    proposer                    CONTROL PLANE                        ESTATE
   (human, CI)  ──proposal──▶  ┌──────────────────┐
                               │ run record       │
                               │ orchestration    │  assume ident.  ┌──────────────┐
                               │ audit            │ ──────────────▶ │  EXECUTION   │
                               └────────┬─────────┘                 │  plan/apply  │──▶ estate
                                        │                           └──────┬───────┘
                    ┌───────────────────┼────────────────┐                 │
                    ▼                   ▼                ▼          plan artifact
              guardrails            reviewer        approval          + document
             (offline, det.)      (advisory)       (identity)               │
                    │                   ▲                                   │
                    │                   └────── projection ◀────────────────┘
                    └──── eligibility ──────────▶ (allow-listed)
                                                        │
                                                        ▼
                                              external surfaces
```

The **control plane never holds the estate's authority**. It can start an execution under a
workspace's identity, and it can record what happened; it cannot change infrastructure itself. This
is the boundary that makes a compromised orchestrator survivable, and it is the reason the delegation
permission is constrained as tightly as the identities it delegates to.

The **plan artifact never leaves the trust boundary**, and the **projection is the only thing that
does**. A plan document is dense with values an operator would not paste into a chat window; the
projection exists so that no component outside the boundary — a reviewer at a third-party inference
provider, a notification surface, a browser — ever receives more than an enumerated set of fields.

**Guardrails run before the reviewer and independently of it.** Their output is an input to the
reviewer's *invocation*, never the reverse. Drawn as a pipeline, the reviewer sits downstream of a
gate it cannot reach back through.

## Normative requirements

### Workspaces, runs, and configuration

- **ZBP-6-R1** A workspace MUST have a stable identifier that is not derived from mutable
  human-facing naming, and MUST map to exactly one state and one execution identity. Two workspaces
  MUST NOT share a state.
- **ZBP-6-R2** A run MUST have an enumerated status drawn from a closed set, and MUST reach exactly
  one terminal status. An implementation MUST NOT represent a run's condition as the absence of a
  record or the presence of a lock alone.
- **ZBP-6-R3** At most one applicable run per workspace MUST be capable of reaching the apply phase at
  a time. This MUST be enforced by a mutual-exclusion mechanism below the API layer, and MUST NOT rely
  on the client refraining from concurrent proposals.
- **ZBP-6-R4** The configuration for a run MUST be captured as an immutable bundle, addressed by a
  cryptographic digest of its content, before the plan phase begins. The plan and apply phases of a
  run MUST use the same bundle.
- **ZBP-6-R5** A run record MUST carry the configuration digest, the provisioning tool version, the
  digest of the dependency lock, the execution identity used, and the identity that proposed it.
- **ZBP-6-R6** A run record MUST be append-only once the run reaches a terminal status. Correcting an
  error MUST produce a new record referencing the old, and MUST NOT rewrite it.
- **ZBP-6-R7** A plan-only run MUST NOT be capable of transitioning to apply under any input. This
  MUST be a property of the run, fixed at proposal, and MUST NOT depend on an approver declining to
  press a control.

### The plan artifact

- **ZBP-6-R8** The plan phase MUST produce a durable plan artifact that the provisioning tool can
  execute without recomputing the change.
- **ZBP-6-R9** The apply phase MUST execute the plan artifact produced by that run's plan phase, and
  MUST NOT compute a new plan. An implementation that re-plans at apply time does not conform,
  however similar the result usually is.
- **ZBP-6-R10** The provisioning tool version MUST be pinned per workspace, recorded on the run, and
  identical across that run's plan and apply phases. A version mismatch MUST fail the apply.
- **ZBP-6-R11** The set of provider or plugin versions MUST be pinned by a dependency lock carried in
  the configuration bundle, and MUST resolve identically in both phases. An implementation MUST NOT
  permit resolution to a newer version between plan and apply.
- **ZBP-6-R12** The identity of the state as read during the plan phase — its lineage and its
  monotonic version counter — MUST be recorded on the run, and MUST be re-verified immediately before
  apply. A mismatch MUST fail the apply, and MUST NOT be reconciled, retried against fresh state, or
  reported as a conflict the approver may override.
- **ZBP-6-R13** A plan artifact MUST have an expiry, and applying an expired artifact MUST fail.
  Expiry MUST be enforced by the control plane rather than by a client declining to submit.
- **ZBP-6-R14** A plan that contains no changes MUST short-circuit: the run MUST reach a successful
  terminal status without evaluating guardrails, invoking a reviewer, or requesting approval.
- **ZBP-6-R15** A plan artifact MUST be treated as secret-bearing. It MUST be encrypted at rest and
  access-controlled no less strictly than the state it was computed against. An implementation MUST
  NOT treat it as a build artifact, publish it to a general artifact store, or attach it to a
  notification.

### Execution identity and isolation

- **ZBP-6-R16** Execution MUST occur under an identity belonging to the workspace, distinct from the
  control plane's own identity.
- **ZBP-6-R17** The control plane's identity MUST NOT be sufficient to modify any estate it
  orchestrates. Its authority over a workspace MUST be limited to starting an execution under that
  workspace's identity and recording the outcome.
- **ZBP-6-R18** The plan phase SHOULD execute under an identity that cannot mutate the estate,
  distinct from the identity used for apply.
- **ZBP-6-R19** No phase of a run MAY use a long-lived credential. Execution identity MUST be
  obtained as a short-lived credential at run time.
- **ZBP-6-R20** Where the control plane delegates execution identity, its permission to do so MUST be
  constrained to an explicitly enumerated set of execution identities. An unconstrained delegation
  permission MUST be treated as equivalent to holding every identity it can delegate.
- **ZBP-6-R21** A workspace MUST hold references to secrets rather than secret values. Dereferencing
  MUST occur inside the execution boundary, under the workspace's execution identity, and MUST NOT
  occur in the control plane.
- **ZBP-6-R22** Secret values MUST NOT be written to the run record, the audit record, or any
  notification. Where execution logs are retained, values known to be secret MUST be masked before
  retention.

### The change projection

- **ZBP-6-R23** A projection MUST be constructed by explicitly enumerating the fields it includes.
  Fields not enumerated MUST NOT be read into it.
- **ZBP-6-R24** An implementation MUST NOT construct a projection by removing known-sensitive fields
  from a plan document. Redaction by removal does not conform, because it fails open on any field the
  removal list does not anticipate, including fields added by a later version of the provisioning
  tool.
- **ZBP-6-R25** Where a plan document provides sensitivity masks shadowing its before and after
  values, those masks MUST be applied to every value the projection includes. An implementation MUST
  handle both mask shapes: a scalar denoting that nothing is sensitive, and a structure mirroring the
  value with sensitive leaves marked.
- **ZBP-6-R26** A value that the plan reports as not yet known MUST be represented in the projection
  as unknown, and MUST NOT be rendered as absent, null, or empty. The distinction between "will be
  computed" and "will be cleared" is material to review.
- **ZBP-6-R27** A projection MUST have a size budget. A plan whose projection would exceed it MUST be
  ineligible for automatic approval. An implementation MUST NOT truncate a projection and proceed.
- **ZBP-6-R28** The projection requirements MUST hold identically for every consumer, regardless of
  whether that consumer executes inside the same trust boundary as the control plane. An
  implementation MUST NOT define a more permissive projection for a reviewer on the grounds of its
  location.
- **ZBP-6-R29** External surfaces — notifications, chat, browsers, third-party inference providers —
  MUST receive the projection and MUST NOT receive the plan document or the plan artifact.

### Deterministic guardrails

- **ZBP-6-R30** Guardrails MUST be evaluated before a reviewer is invoked, MUST be deterministic
  functions of the plan document and the workspace policy, and MUST NOT depend on a network call.
- **ZBP-6-R31** Guardrails MUST decide eligibility, and eligibility MUST be a hard precondition for
  automatic approval. A workspace policy MUST be able to express, at minimum: destruction or
  replacement of a named resource class; changes that widen access; a bound on the number of
  resources changed; and a bound on projection size.
- **ZBP-6-R32** A guardrail that fails to evaluate MUST render the run ineligible. An implementation
  MUST NOT treat an evaluation error as an absence of findings.
- **ZBP-6-R33** The identity and version of the guardrail rule set MUST be recorded on the run. A
  change to the rule set MUST NOT alter the recorded outcome of a completed run.
- **ZBP-6-R34** Automatic approval MUST be disabled by default and MUST be enabled per workspace by
  explicit configuration. Enabling it MUST be an auditable change.
- **ZBP-6-R35** A workspace that manages the review gate's own infrastructure, its guardrail rule set,
  or its policy MUST NOT be eligible for automatic approval.

### The reviewer

- **ZBP-6-R36** A reviewer MUST be invoked only for a run that guardrails have found eligible.
- **ZBP-6-R37** A reviewer's output MUST be capable only of withholding an approval that would
  otherwise proceed. Reviewer output MUST NOT be able to cause a run to be approved that guardrails
  found ineligible, to alter the guardrail outcome, or to widen the set of runs eligible for
  automatic approval. This requirement is structural: an implementation MUST NOT satisfy it by
  instructing the reviewer to refuse.
- **ZBP-6-R38** Plan content MUST be presented to a reviewer as data, within a delimited region, and
  the reviewer's instructions MUST state that content within that region is never an instruction. This
  requirement is secondary to ZBP-6-R37 and MUST NOT be relied on as the primary control.
- **ZBP-6-R39** A reviewer MUST NOT be given credentials, network access to the estate, or any tool
  whose invocation has an effect outside the review. A successful manipulation of a reviewer MUST
  yield nothing beyond an incorrect verdict.
- **ZBP-6-R40** A verdict MUST be structured, and MUST be parsed strictly against a schema. A verdict
  that does not parse MUST escalate.
- **ZBP-6-R41** A reviewer error, timeout, refusal, rate limit, or unavailability MUST escalate. An
  implementation MUST NOT retry indefinitely, and MUST NOT treat exhaustion of retries as approval.
- **ZBP-6-R42** The verdict, the reviewer implementation, the model identity where applicable, and a
  digest of the exact projection submitted MUST be recorded on the run.
- **ZBP-6-R43** The reviewer MUST be replaceable without altering guardrails, approval, or the audit
  record. An implementation MUST provide a reviewer that always escalates, and MUST behave correctly
  when configured with it.
- **ZBP-6-R44** A reviewer MUST NOT be given the guardrail outcome as a value it can contradict, and
  guardrail evaluation MUST NOT consume reviewer output. The two MUST NOT be combined into a single
  scored decision in which a sufficiently confident reviewer outweighs a guardrail.

### Approval and identity

- **ZBP-6-R45** An approval MUST be attributable to an authenticated identity that is authorized to
  apply that workspace. Authorization to propose a run MUST be separable from authorization to
  approve one.
- **ZBP-6-R46** Approval MUST be idempotent with respect to a run. The first decision to reach the
  control plane MUST be the decision; subsequent decisions MUST be recorded and MUST NOT take effect.
- **ZBP-6-R47** An implementation SHOULD support requiring that the approving identity differ from
  the proposing identity, configurable per workspace.
- **ZBP-6-R48** Every surface through which an approval may be given MUST enforce the same
  authorization. A surface MUST NOT confer approval authority that the same identity would not hold
  through the API.
- **ZBP-6-R49** A surface that cannot use the control plane's primary authentication MUST verify
  message authenticity by a mechanism bound to the surface, MUST reject messages outside a bounded
  time window, and MUST reject replays within it.
- **ZBP-6-R50** Where an external surface carries its own identifiers, those identifiers MUST be
  mapped to an approver identity by control-plane configuration. An identifier presented in a payload
  MUST NOT be trusted as an identity.
- **ZBP-6-R51** An approval MUST fail if the run's plan artifact has expired, the run is not awaiting
  approval, or the run has already reached a terminal status.

### Audit and notification

- **ZBP-6-R52** Every run MUST leave a durable record of the proposing identity, the configuration
  digest, the guardrail outcome and rule-set version, the verdict and reviewer identity where one
  ran, the approving identity or the fact of automatic approval, and the terminal outcome.
- **ZBP-6-R53** Execution logs MUST be retained beyond the lifetime of the execution environment, and
  MUST be retrievable for a completed run.
- **ZBP-6-R54** A notification MUST carry only the projection and identifiers. It MUST NOT carry the
  plan document, the plan artifact, or an unprojected rendering of the change.
- **ZBP-6-R55** A scheduled or drift-detection plan MUST be an ordinary plan-only run subject to the
  same guardrails, projection, and audit requirements.
- **ZBP-6-R56** The gate MUST NOT be the only path by which infrastructure can be changed in an
  emergency. A break-glass path MUST exist that does not depend on the control plane, the reviewer,
  or their dependencies, and its use MUST be auditable after the fact.

## Wire formats and interfaces

### The projection

A projection is a JSON document. Its shape is fixed so that a consumer — a reviewer prompt, a
notification renderer, a conformance test — reads one format regardless of the provisioning tool
underneath. Every value appears as a *value representation*, exactly one of three forms:

```json
{"known": "t3.micro"}
{"unknown": true}
{"sensitive": true}
```

The three are disjoint on purpose. Collapsing "sensitive" into "unknown" loses the distinction
between a value that will be computed and a value that exists but may not be shown, and an approver
reads those differently.

```json
{
  "format": 1,
  "counts": { "create": 1, "update": 0, "delete": 0, "replace": 0 },
  "resources": [
    {
      "address": "random_pet.name",
      "type": "random_pet",
      "actions": ["create"],
      "attributes": {
        "prefix":    { "after": {"known": "team-platform"} },
        "id":        { "after": {"unknown": true} }
      }
    }
  ],
  "outputs": [
    { "name": "plain_out", "actions": ["create"], "after": {"known": "not-a-secret"} },
    { "name": "secret_out", "actions": ["create"], "after": {"sensitive": true} }
  ]
}
```

### The source allow-list

A conforming projection reads these paths of a plan document and no others. The list is the
interface; extending it is a specification change, not an implementation detail.

| Source path | Use |
|---|---|
| `resource_changes[].address` | resource identity |
| `resource_changes[].type` | resource class, for guardrail and reviewer context |
| `resource_changes[].change.actions` | the action set |
| `resource_changes[].change.before` / `.after` | attribute values, before masking |
| `resource_changes[].change.before_sensitive` / `.after_sensitive` | sensitivity masks |
| `resource_changes[].change.after_unknown` | unknown-value mask |
| `output_changes[].actions`, `.after`, `.after_sensitive`, `.after_unknown` | output changes |

Everything else in a plan document — the variables supplied to the run, the configuration, the
planned values, the prior state — is **never read**. That is the whole mechanism: the sensitive
paths are not removed, they are never visited.

### The verdict

```json
{
  "decision": "approve",
  "reasoning": "Tag-only change on two resources; no replacement, no access change.",
  "findings": [
    { "severity": "info", "address": "random_pet.name", "note": "prefix changed" }
  ]
}
```

`decision` is `approve` or `escalate` and nothing else. A reviewer has no vocabulary for "reject",
because rejection and escalation are the same action — a human decides — and offering both invites
an implementation to treat one as final.

## Algorithms

### Constructing a projection

Given a plan document `D`, produce projection `P`, reading only the allow-listed paths.

1. Initialise `P` with `format`, zeroed `counts`, and empty `resources` and `outputs`.
2. For each entry `c` in `D.resource_changes`:
   1. Take `address`, `type`, `actions`. If `actions` is `["no-op"]`, skip the entry.
   2. Increment the count for the action: a `["delete","create"]` or `["create","delete"]` action
      pair counts as `replace`, not as one of each.
   3. For each attribute key `k` present in `c.change.after` or `c.change.after_unknown`:
      1. If `mask(c.change.after_sensitive, k)` is true, emit `{"sensitive": true}`.
      2. Else if `mask(c.change.after_unknown, k)` is true, emit `{"unknown": true}`.
      3. Else emit `{"known": c.change.after[k]}`.
   4. Apply the same three steps to `before` using `before_sensitive`, where a before value exists.
3. For each entry in `D.output_changes`, apply step 2.3 to its `after`.
4. If the serialized size of `P` exceeds the budget, return *over-budget*. Do not truncate.

`mask(m, k)` is defined for both mask shapes: if `m` is `false` or absent, the result is false; if
`m` is `true`, the result is true; if `m` is an object, the result is `mask(m[k], …)` recursively,
and absent keys are false. A mask that is `true` at a branch marks the whole subtree.

Failure outcomes: *over-budget* renders the run ineligible under ZBP-6-R27. A plan document that
cannot be parsed renders the run ineligible under ZBP-6-R32, and MUST NOT be reported as an empty
change set — an unparseable plan and a plan with no changes are opposite conclusions.

### Deciding a run

1. Execute the plan phase; capture the plan artifact, the plan document, and the state identity.
2. If the plan reports no changes, terminate successfully (ZBP-6-R14).
3. Construct the projection. On over-budget, mark ineligible.
4. Evaluate guardrails against the plan document. Any veto marks ineligible. An evaluation error
   marks ineligible (ZBP-6-R32).
5. If the workspace does not permit automatic approval, or the run is ineligible, request human
   approval and wait.
6. Otherwise invoke the reviewer with the projection and the workspace policy. On `approve`,
   proceed. On `escalate`, on a malformed verdict, or on any error, request human approval and wait.
7. On approval — human or automatic — re-verify the state identity and the artifact's expiry. On any
   mismatch, fail the run (ZBP-6-R12, ZBP-6-R51).
8. Execute the plan artifact. Record the terminal outcome.

Step 7 is deliberately after approval rather than before. Checking freshness at the moment of
decision proves nothing about the moment of execution, and the gap between them is exactly where a
competing apply lands.

## Reviewer bindings

The reviewer contract is a single call: projection and policy in, verdict out. Everything that
varies between providers — transport, authentication, how a structured response is obtained — sits
below that line, which is why ZBP-6-R43 requires the reviewer to be replaceable without touching
guardrails or approval.

- **In-account inference.** The execution identity authenticates directly; no key is stored, which
  satisfies ZBP-6-R19 without additional machinery. The projection remains subject to ZBP-6-R28: an
  in-account reviewer receives exactly what a third-party one would.
- **A first-party model API.** Requires a credential held as a reference and dereferenced under
  ZBP-6-R21. Structured output is obtained by a schema-constrained response or a tool call, and
  validated again on receipt regardless — ZBP-6-R40 is a requirement on the consumer, not a
  statement of trust in the provider.
- **An aggregating gateway.** Adds a party to the trust boundary. Conformance is unchanged, but the
  boundary the projection defends is now wider, and the workspace policy SHOULD say so.
- **A local model.** No external boundary is crossed at all, which does not relax any requirement,
  because ZBP-6-R37 exists to contain a compromised reviewer rather than an eavesdropped one.
- **The always-escalate reviewer.** Required by ZBP-6-R43. It is the correct configuration for a
  workspace whose changes are all dangerous, and it is the control against which the rest are tested.

A binding MUST NOT introduce a fourth verdict, a confidence score consumed as a threshold, or a
retry loop that terminates in approval. Each is a way of reintroducing the widening this
specification forbids.

## AWS binding

The requirements above are cloud-neutral. This is the concrete mapping, and three limits that are
properties of the platform rather than of the design.

| Concern | Service | Notes |
|---|---|---|
| Run lifecycle | Step Functions, Standard class | One execution per run, one state per phase. Executions run up to a year, so the wait between a planned and a decided run needs no durable-timer machinery of its own. |
| Approval gate | A `.waitForTaskToken` state | Approval becomes an inbound event that resumes the execution, rather than a status column swept by a schedule. `TimeoutSeconds` on that state is ZBP-6-R13. |
| Execution | ECS Fargate via `ecs:runTask.sync` | One task definition; `overrides.taskRoleArn` supplies the workspace identity per invocation, so ZBP-6-R16 needs no task definition per workspace. 20 GB of ephemeral storage by default covers provider downloads. |
| Run records, locking | DynamoDB, one item per run | A conditional write on a per-workspace item is the mutual exclusion of ZBP-6-R3. |
| Configuration, artifacts | S3 | Configuration keyed by content digest and written by the proposer through a pre-signed `PUT`, satisfying ZBP-6-R4 without granting bucket-wide access. Artifacts encrypted under the state's key policy, per ZBP-6-R15. |
| Logs | CloudWatch Logs via the `awslogs` driver | Tailed live by filtering on the run id; copied to S3 on completion for ZBP-6-R53. |
| State | S3 with conditional-write locking | `use_lockfile = true` from OpenTofu 1.10 onward removes the separate lock table entirely. |
| Secrets | SSM Parameter Store, by reference | Resolved inside the task under the workspace identity, never by the orchestrator (ZBP-6-R21). |
| API | API Gateway HTTP API | `AWS_IAM` on the primary routes; a separate route with `NONE` and its own signature verification for surfaces that cannot sign requests (ZBP-6-R49). |

### Three platform limits

**A state payload is capped at 256 KB.** Exceeding it fails the execution with
`States.DataLimitExceeded`, and the cap covers input and output together. This is why every artifact
in this design travels by reference: the plan artifact, the plan document and the log are S3 keys on
the execution's state, never values in it. It also gives the projection budget of ZBP-6-R27 a
concrete ceiling rather than an arbitrary one — a projection is the one derived object that may
plausibly be carried inline, and the moment it cannot be, the run is over-budget by definition. The
same 256 KB applies to the `output` of `SendTaskSuccess`, so an approval decision carries an
identifier and a verdict, not a rendering.

**`ecs:RunTask` together with `iam:PassRole` is a privilege-escalation path.** Overriding the task
role per run is what makes one task definition serve every workspace, and it is also what makes the
orchestrator's `iam:PassRole` the most dangerous permission in the system. Scoped to `*`, the
orchestrator can pass itself any role in the account, and ZBP-6-R17's isolation is decorative — the
shape [CWE-269](ref:cwe-269) describes. Constrain it to an explicit resource list or a path prefix
covering exactly the workspace execution roles, and write the trust policy of each of those roles to
name the ECS tasks service principal and nothing else. This is the single control on which the
threat model's "compromised control plane" analysis depends.

**A task token outlives nothing and expires from nothing.** Tokens are at most 2048 characters and
carry no intrinsic lifetime; an approval state without `TimeoutSeconds` waits until the execution's
one-year ceiling, holding the workspace lock and billing for the privilege. The expiry in ZBP-6-R13
is therefore not only a safety property but the only thing preventing a forgotten run from blocking
its workspace indefinitely.

### Backend injection

The workspace's source need not declare a backend at all. Writing an override file into the working
directory before `init` — `{"terraform":[{"backend":[{"s3":[{}]}]}]}` as `_override.tf.json` — and
supplying bucket, key and locking through `-backend-config` initializes cleanly, and also overrides a
backend the source does declare. Verified against OpenTofu 1.11.6.

The property this buys is worth more than the tidiness: source carrying no marker tying it to the
gate remains executable by hand by any operator holding the execution identity, which is what makes
ZBP-6-R56's break-glass path nearly free. A design that requires a proprietary block in the source —
the shape hosted offerings generally take — makes the escape hatch a migration.

## Threat model and failure modes

**A reviewer manipulated through plan content.** The plan document contains strings an attacker can
often write: resource names, tags, descriptions, commit messages surfaced as metadata. Instruction-level
defences against this are probabilistic. The design's answer is that a manipulated reviewer can only
produce a verdict, and the only verdict that changes an outcome is one that withholds approval from a
run guardrails already found eligible. The worst achievable result is an unnecessary escalation.

**A compromised control plane.** It can start executions, so it can cause a *previously approved*
artifact to be applied, and it can lie in the audit record. It cannot construct a new change, because
it holds no estate authority (ZBP-6-R17) and cannot forge an artifact the tool will execute. This
degrades to "the attacker can replay a change you approved", which is why ZBP-6-R13 bounds artifact
lifetime. If ZBP-6-R20 is violated, this analysis collapses entirely and the control plane becomes
equivalent to every identity it may delegate.

**A racing apply.** Two runs plan against the same state; one applies; the second's artifact is now
computed against a state that no longer exists. Detection is ZBP-6-R12, and the check must happen
after approval (step 7) because a pre-approval check leaves the whole approval window uncovered.

**A stale or substituted artifact.** Version drift between plan and apply produces an artifact the
tool will refuse or, worse, interpret differently. ZBP-6-R10 and ZBP-6-R11 close this; the failure
mode they prevent is subtle because most version differences are harmless most of the time.

**Leakage through the projection.** The dominant failure is a projection built by removal. In a
representative plan document, the value of one variable marked sensitive appears in five distinct
locations, of which one carries a sensitivity flag; the other four are in sections a removal-based
implementation has no reason to visit. ZBP-6-R23 and ZBP-6-R24 exist because of this specific shape.

**The gate as an incident dependency.** A review gate sits directly in the path of the fix during an
incident, and its own dependencies — the workflow engine, the reviewer's provider, the notification
surface — are things that can be down. ZBP-6-R56 requires a path around it.

**Approval replay.** An approval message captured from an external surface and resubmitted. ZBP-6-R49
bounds the window and rejects replays; ZBP-6-R46 makes a second approval inert even if one succeeds.

## Anti-patterns

**Re-planning at apply.** The most common shape, because it is what running the tool twice does
naturally, and because the diff is usually identical. It means no one ever approved what executed.

**Treating the reviewer as an approver.** Expressed as "the model approves low-risk changes". It
inverts the ordering, and makes every string in the plan an input to an authorization decision.

**A confidence threshold.** A reviewer returning a score, with approval above a cutoff. This is
widening with extra steps: raising the score is exactly what a manipulation attempts.

**Redaction by removal.** Enumerating the sensitive paths and deleting them. It fails open on the
first field the enumeration missed, and provisioning tools add fields.

**Truncating an oversized projection.** The plans too large to summarize are disproportionately the
plans that most need review.

**Auto-approving the gate's own workspace.** A change to the guardrail rule set that the guardrail
rule set evaluates. ZBP-6-R35 forbids it.

**One identity for plan and apply.** Convenient, and it means a read-only operation is performed with
authority to destroy the estate.

**Reporting an unparseable plan as no changes.** An empty change set and a failure to parse are
opposite conclusions, and conflating them makes a broken plan look like a clean one.

## Test vectors

Vectors are given against a plan document in the format documented for a widely used provisioning
tool. `V1` and `V2` are taken from a plan actually executed against OpenTofu 1.11.6, of a
configuration declaring one variable marked sensitive with the value `hunter2-super-secret`, one
resource with a sensitive computed attribute, one ordinary resource, and two outputs of which one is
sensitive. `V3`–`V14` are constructed inputs covering mask shapes and decision paths the executed
plan does not exhibit. All fourteen were verified against a reference implementation of the
algorithms above.

**V1 — the five locations.** In that plan document, the string `hunter2-super-secret` occurs at:

```
.variables.db_password.value
.configuration.root_module.variables.db_password.default
.planned_values.outputs.secret_out.value
.prior_state.values.outputs.secret_out.value
.output_changes.secret_out.after
```

Only the last carries a sensitivity flag (`output_changes.secret_out.after_sensitive: true`).
**Expected:** a conforming projection of this document contains the string zero times.

**V2 — sensitive computed attribute.** Input change fragment:

```json
{ "address": "random_password.pw", "type": "random_password",
  "change": { "actions": ["create"], "before": null,
    "after": { "length": 16, "special": true },
    "after_unknown": { "id": true, "result": true, "bcrypt_hash": true },
    "before_sensitive": false,
    "after_sensitive": { "result": true, "bcrypt_hash": true } } }
```

**Expected:** `result` and `bcrypt_hash` project as `{"sensitive": true}` — sensitivity is checked
before unknownness, so a value that is both is reported as sensitive. `id` projects as
`{"unknown": true}`. `length` projects as `{"known": 16}`.

**V3 — scalar mask.** `"before_sensitive": false` with a populated `before`. **Expected:** every
`before` value projects as `{"known": …}`; the scalar `false` is handled without attempting a key
lookup.

**V4 — nested mask.** `"after_sensitive": {"config": {"password": true}}` against
`"after": {"config": {"user": "svc", "password": "s3cr3t"}}`. **Expected:** `config.user` projects
known; `config.password` projects `{"sensitive": true}`.

**V5 — subtree mask.** `"after_sensitive": {"config": true}` against the same `after`.
**Expected:** the whole `config` subtree projects as `{"sensitive": true}`; no leaf beneath it is
emitted.

**V6 — replacement counting.** `"actions": ["delete", "create"]`. **Expected:** `counts.replace` is
1; `counts.delete` and `counts.create` are 0.

**V7 — no-op exclusion.** `"actions": ["no-op"]`. **Expected:** the entry contributes no resource and
no count.

**V8 — eligibility, protected class.** A workspace policy protecting `random_password`, against a
change with `"actions": ["delete","create"]` on `random_password.pw`. **Expected:** ineligible, reason
`protected-resource-replaced`.

**V9 — eligibility, count bound.** A policy bounding changed resources at 10, against a plan with 11
non-no-op changes. **Expected:** ineligible, reason `change-count-exceeded`.

**V10 — eligibility, clean.** A policy as in V8 and V9, against a single `update` on `random_pet.name`
touching only `prefix`. **Expected:** eligible.

**V11 — over-budget.** A projection serializing above the configured budget. **Expected:**
ineligible, reason `projection-over-budget`, and no truncated projection is produced.

**V12 — unparseable plan.** A plan document that is not valid JSON. **Expected:** ineligible, reason
`plan-unparseable`. Distinguishable from V13.

**V13 — no changes.** `resource_changes` present and empty. **Expected:** the run terminates
successfully without guardrail evaluation, per ZBP-6-R14.

**V14 — malformed verdict.** A reviewer returning `{"decision": "reject"}`. **Expected:** escalate;
`reject` is not in the verdict vocabulary and MUST NOT be interpreted as either outcome.

## Conformance checklist

**Runs and configuration**

- [ ] Workspace identifiers are stable and map to exactly one state (ZBP-6-R1)
- [ ] Run status is drawn from a closed set with exactly one terminal value (ZBP-6-R2)
- [ ] Concurrency is enforced below the API, not by client discipline (ZBP-6-R3)
- [ ] Configuration is immutable and content-addressed before planning (ZBP-6-R4)
- [ ] The run record carries config digest, tool version, lock digest, and both identities (ZBP-6-R5)
- [ ] Terminal run records are append-only (ZBP-6-R6)
- [ ] A plan-only run cannot transition to apply under any input (ZBP-6-R7)

**The plan artifact**

- [ ] The plan phase produces a durable executable artifact (ZBP-6-R8)
- [ ] Apply executes that artifact and never re-plans (ZBP-6-R9)
- [ ] Tool version is pinned, recorded, and identical across phases (ZBP-6-R10)
- [ ] Provider versions resolve identically in both phases (ZBP-6-R11)
- [ ] State identity is captured at plan and re-verified before apply; mismatch fails (ZBP-6-R12)
- [ ] Artifacts expire, and expiry is enforced server-side (ZBP-6-R13)
- [ ] A zero-change plan short-circuits to success (ZBP-6-R14)
- [ ] Artifacts are encrypted and access-controlled as state is (ZBP-6-R15)

**Execution identity**

- [ ] Execution uses a workspace identity distinct from the control plane's (ZBP-6-R16)
- [ ] The control plane cannot modify any estate it orchestrates (ZBP-6-R17)
- [ ] No long-lived credential is used by any phase (ZBP-6-R19)
- [ ] Delegation permission is constrained to an enumerated set (ZBP-6-R20)
- [ ] Secrets are held by reference and dereferenced inside execution (ZBP-6-R21)
- [ ] Secret values never reach run records, audit, or notifications (ZBP-6-R22)

**Projection**

- [ ] The projection is built from an enumerated allow-list (ZBP-6-R23)
- [ ] No removal-based redaction is used anywhere (ZBP-6-R24)
- [ ] Both mask shapes are handled (ZBP-6-R25)
- [ ] Unknown values are represented as unknown, not absent (ZBP-6-R26)
- [ ] Over-budget projections make a run ineligible and are never truncated (ZBP-6-R27)
- [ ] The projection is identical for in-boundary and out-of-boundary consumers (ZBP-6-R28)
- [ ] External surfaces receive only the projection (ZBP-6-R29)

**Guardrails**

- [ ] Guardrails run before the reviewer, deterministically and offline (ZBP-6-R30)
- [ ] Policy expresses destruction, access widening, count bounds, and size bounds (ZBP-6-R31)
- [ ] A guardrail evaluation error renders the run ineligible (ZBP-6-R32)
- [ ] Rule-set version is recorded and completed runs are not retroactively altered (ZBP-6-R33)
- [ ] Automatic approval is off by default and enabled per workspace auditably (ZBP-6-R34)
- [ ] The gate's own workspaces are never eligible (ZBP-6-R35)

**Reviewer**

- [ ] The reviewer runs only on eligible runs (ZBP-6-R36)
- [ ] No reviewer output can widen eligibility, structurally (ZBP-6-R37)
- [ ] Plan content is delimited and declared non-instructional (ZBP-6-R38)
- [ ] The reviewer holds no credentials and no side-effecting tools (ZBP-6-R39)
- [ ] Verdicts are strictly parsed; unparseable escalates (ZBP-6-R40)
- [ ] Every reviewer failure mode escalates (ZBP-6-R41)
- [ ] Verdict, reviewer, model identity, and projection digest are recorded (ZBP-6-R42)
- [ ] An always-escalate reviewer exists and the system works with it (ZBP-6-R43)
- [ ] Guardrails and reviewer are not merged into one scored decision (ZBP-6-R44)

**Approval**

- [ ] Approval is attributable and separately authorized from proposal (ZBP-6-R45)
- [ ] Approval is idempotent; the first decision wins (ZBP-6-R46)
- [ ] Every surface enforces identical authorization (ZBP-6-R48)
- [ ] Non-primary surfaces verify authenticity, bound time, and reject replays (ZBP-6-R49)
- [ ] External identifiers map to identities by configuration, not by payload (ZBP-6-R50)
- [ ] Approval fails on expired, non-pending, or terminal runs (ZBP-6-R51)

**Audit**

- [ ] Every run leaves the full durable record (ZBP-6-R52)
- [ ] Logs outlive the execution environment (ZBP-6-R53)
- [ ] Notifications carry only the projection and identifiers (ZBP-6-R54)
- [ ] Scheduled and drift plans are ordinary plan-only runs (ZBP-6-R55)
- [ ] A break-glass path exists that does not depend on the gate (ZBP-6-R56)

## Rationale

The rationale for provisioning declaratively, and for the tool this specification is shaped around,
is in ZFN-42. The credential requirements restate nothing: ZFN-9 argues the position, ZBP-2
specifies one issuance mechanism among several, and this document only says what execution may hold. ZFN-35 is why a
workspace stores a reference rather than a value. ZFN-55 is why the execution identity is
per-workspace at all — the blast radius of a compromised gate is bounded by what it may delegate, and
that is a decision made when the identities are created, not when a run starts. ZFN-64 is why a plan
is treated as a deploy rather than as an administrative side channel. ZFN-4 is why ZBP-6-R56 exists,
and why it is a MUST rather than an operational nicety.

The one argument this document does make for itself concerns ordering. A reviewer is genuinely
useful: most changes are unremarkable, deterministic rules cannot read intent, and a model can tell a
tag edit from a tag edit that also replaces a database. But usefulness is not authority. The moment a
model's output can widen what is permitted, every string a plan can carry becomes an input to an
authorization decision, and plan documents carry strings that attackers write. Putting the
deterministic layer first and giving the reviewer only a veto costs almost nothing — the changes it
would have widened were the dangerous ones — and it converts an unbounded class of prompt-injection
outcomes into, at worst, an unnecessary page to a human.

## Changelog

- **2026-08-23** (1.0.0): First published. Specifies the run lifecycle and configuration capture, the
  plan artifact as the unit of approval with version pinning and state-identity verification,
  execution identity and delegation limits, the allow-list projection and its mask handling,
  deterministic guardrails deciding eligibility, the reviewer contract and its uniform escalation on
  failure, approval and identity across surfaces, and the audit record. Includes vectors V1–V14, of
  which V1 and V2 are taken from an executed plan and the remainder are constructed.
