Skip to content

Deployment Preflight (feasibility and blocker collection)

Before a deployment runs (terraform apply, or a control-plane fix PR), use the deploy-preflight pass. It collects everything in the target environment that could block or degrade the deployment, grounds each item in the exact rule that produced it, and maps it to the concrete lever that clears it. It is the what-if verifier generalized from a single action to a whole deployment.

This resolves a recurring class of failures - a plan that is correct in isolation but is rejected by the target subscription’s guardrails: a denied resource type, a blocked package or image source, a missing role assignment, an exhausted quota, or a dependency that must exist before the resource it supports. Instead of discovering these one at a time as terraform apply fails, the preflight pass reports them all at once, up front.

Customer-agnostic: every denylist, blocked host, mirror endpoint, and toggle value below is supplied by config or a fork - the upstream ships the machinery and generic taxonomy, never a customer’s specific guardrail values (generic-scope.instructions.md).

The design defines two entry points that share one analyzer. The protected human deploy path is shipped through a standalone runner script; the control-plane path currently stops at the seam:

  • Control plane (partly shipped): before the executor emits a fix PR, the analyzer checks that the change would actually land in the target scope. A blocking detected issue degrades the action to hil rather than opening a PR that would fail policy. The deterministic gate that enforces this ordering and a delivery wrapper for exact-patch refresh exist and are tested; no live path binds the required refresh yet.
  • Human deploy (shipped): the private-runner workflow creates the report before plan and binds its evidence digest into exact-plan metadata. PR comment/GitHub Check delivery remains a follow-up.

Both paths are deterministic-first (T0-flavored): static analysis with no cloud calls resolves most detected issues; bounded, read-only live probes confirm the rest (egress reachability, quota). Nothing in the pass mutates anything.

A probe inspects a PreflightTarget (the scope plus the resource types, egress hosts, and required links a deployment intends to touch) and returns grounded detected issues in one category. The generic catalog:

CategoryRepresentative blockerDetection (deterministic-first)
policy_guardraildisallowed resource types, NSG required, inline disk denied, public IP deniedterraform plan JSON re-checked against policies/ (OPA) + Azure Policy deny simulation (static)
supply_chain_egressdocker.io blocked, PyPI / npm / apt blocked, external base image pull deniedNSG / Firewall / UDR rule analysis (static) + bounded egress reachability probe (live)
identity_rbacexecutor identity lacks a role on the target scope; cannot create a role assignmentscope role-assignment check from the inventory graph (static)
quota_capacitySKU / region quota exceeded, zone capacity unavailablequota lookup (live, cached)
dependency_orderingdisk before VM, NSG before subnet, private endpoint before resourceordering violation derived from policy + the module dependency graph (static)
secret_configKey Vault reference unresolvable, required secret absentsecret existence / reachability check (static)

The policy_guardrail and supply_chain_egress categories are the two the hardened-network customers hit most: they map directly to the Azure Policy deny guardrails (Not allowed resource types / Allowed resource types) and to firewall egress denylists. See rule-catalog-collection.md for how the underlying rules are sourced.

Detected issues are assembled into one DeploymentReadinessReport (core/deploy_preflight/report.py). Each detected issue carries three required parts:

  • evidence - a CSP-neutral citation of the rule that produced it (policy:<neutral-id>, nsg:<neutral-id>/rule:<name>). A probe that cannot cite a source MUST NOT emit a detected issue; an ungrounded blocker is a defect, the same rule the T2 verifier follows.
  • severity - blocking (gates an enforce-mode deploy) or warning (surfaces but never gates).
  • resolution - how to clear it, mapped to a concrete lever when possible (see the toggle table below).
DecisionMeaning
clearno detected issues
needs_reviewdetected issues exist but none is blocking (warnings only)
blockedat least one blocking detected issue

The report always records the truthful decision. Whether that decision gates a deploy is a separate flag, blocks_deploy, which is true only when the pass ran in enforce mode.

Every new probe ships in observation mode: it reports blockers truthfully but blocks_deploy stays false, so an unproven probe can never wrongly stop a human deploy on a false positive. A probe is promoted to enforce per-category only after its false-positive rate is measured on the frozen scenario set - the same promotion discipline the ActionType contract applies to autonomous actions.

A cleared reassembly is a decision taken at some earlier moment. Before any fix proposal is submitted, the gate re-runs the same analyzer on the accumulated overrides and withholds publication whenever the fresh report cannot justify it:

HoldCause
no_applied_togglethe loop applied no toggle; the pass is a no-op rather than a rejection
reassembly_escalatedthe loop escalated; a partial reassembly is never submitted
blocking_findingthe re-verified report still carries a blocking detected issue
stale_evidencethe report is outside the freshness window or its timestamp is unusable
scope_driftthe report or an applied toggle does not bind the expected scope

Every hold is decided before the first submission, so a withheld pass submits nothing rather than a partial proposal set, and the truthful decision gates publication even in observation mode where blocks_deploy stays false. A verifier that raises propagates before any submission; the caller routes the pass to hil.

For the PR delivery seam, a separate wrapper re-runs a caller-provided read-only plan refresh on every publish attempt, including redelivery. It requires the verified patch SHA-256 to equal the immutable proposed patch, the report scope to equal the caller’s trusted scope, and all required probe categories to be checked. Missing refresh, changed patch, incomplete coverage, stale evidence, or a blocker withholds the PR even in observation mode. The wrapper cannot prove a provider re-probed by checking a timestamp alone: the caller still owes an authoritative re-render and read-only reanalysis after source or Inventory changes. The current PR publisher does not yet bind the refresh to its subsequent base-branch resolution, so a source change between those operations remains a live-composition blocker. An unbound wrapper is not a production gate.

The GitHub Checks adapter can annotate an existing PR’s exact head revision. It exposes only mode, decision, capped detected issue count, and checked-category count; raw scope, evidence, detected issue text, and caller metadata stay private. Observation mode reports are advisory. A missing adapter or failed Check does not certify preflight and cannot undo an already opened PR. GitHub does not offer an atomic create-if-absent Check call across processes; live composition needs single-writer delivery or a durable lock before claiming distributed idempotency.

A report is not just a list of problems; each terraform_toggle detected issue names the infra sub-module and variable override that makes the deployment comply. This reuses the existing infra/modules/<seam>/ + var.<seam>_kind selection pattern (project-structure.md), generalized to resource-provisioning modes so the module output contract stays fixed while its internal wiring switches:

ToggleValuesEffect
disk_provisioninginline | attach_existingcreate the VM disk inline vs attach a pre-provisioned disk (var.existing_disk_ids)
nsg_provisioningcreate | byocreate an NSG vs reference an existing one (var.existing_nsg_id), attached as the guardrail requires
registry_sourcedocker_io | acr_mirrorpull base images from an internal registry mirror instead of docker.io
python_index_url(string)point package installs at an internal PyPI mirror / artifact feed
dependency_orderingstrictsplit prerequisite resources (disk, NSG, private endpoint) into an ordered apply stage

The mapping is what makes a denied resource type a non-problem: an inline-disk deny resolves to disk_provisioning=attach_existing, so the plan never emits the denied operation in the first place. When a resolution is marked autofix, the analyzer may propose the toggle change as a fix PR without human judgment; otherwise it emits guidance and routes to review.

PieceLocationRole
Probe seamshared/providers/feasibility_probe.pyFeasibilityProbe Protocol + detected issue / target dataclasses
Generic probesshared/providers/local/feasibility.pydeterministic, config-driven upstream defaults (no network)
Orchestratorcore/deploy_preflight/analyzer.pyfan out over probes, assemble the report (fail-closed)
Reportcore/deploy_preflight/report.pythe assembled artifact + decision + blocks_deploy
Pre-publication gatecore/deploy_preflight/pre_publication_gate.pyre-verify before proposal submission; withhold and route to human review
PR delivery wrapperdelivery/deploy_preflight/pr_publication.pyrequire an exact-patch refresh before the injected PR publisher
GitHub Checks adapterdelivery/github/preflight_checks.pypost a bounded status to an existing PR head, without approval authority

core/ sees only the FeasibilityProbe Protocol; the probes are injected at the composition root via the Container.feasibility_probes seam. The upstream default binds no probes (the denylists are customer config); a fork or a live Azure adapter registers its own without editing core/.

  • Fail-closed - a probe that raises propagates; the pass never reports clear on a partial run. A blocking detected issue degrades a control-plane action to hil, never to an ungated auto-action.
  • Read-only - probes never mutate; the pass is safe to run on every deploy.
  • Idempotent - detected issues are ordered deterministically (blocking first, then by id), so a re-run over the same inputs produces a byte-identical report.
  • Grounded - no detected issue without evidence citing its source rule.
  • Discovery feedback - recurring blockers across environments (for example, every scope blocks docker.io) are a signal to the discovery loop to propose a new default toggle or rule (architecture.instructions.md § Rule Catalog).

Shipped: the probe seam, generic deterministic probes, analyzer + report, standalone Azure preflight script, protected-plan evidence binding in the deploy workflow, and tests.

  1. Azure probes and protected-plan evidence (shipped): a shared read-only ARM client (AzureArmClient, injected httpx.AsyncClient + WorkloadIdentity bearer token, fail-closed) plus the AzurePolicyGuardrailProbe (real Azure Policy deny guardrails - Not allowed / Allowed resource types) and the AzureQuotaProbe (Compute usages per subscription + location) have landed with mock-HTTP unit tests. The policy parser accepts the built-in allOf type constraint only when every sibling is the canonical type-exists guard; unknown siblings remain fail-closed. Isolated live validation proved one RG-scoped disk deny maps to disk_provisioning=attach_existing, while a real quota shortage remains a manual blocker; the temporary assignment was removed through its reviewed rollback. scripts/deployment/azure/run_live_preflight.py composes them through the same analyzer with Azure CLI workload identity, bounded read-only ARM transport, neutral-to-ARM type mapping, and sanitized fail-closed errors. The existing Resource Graph role observer is also composed through AzureIdentityRbacProbe to report missing event-bus and secret-reader executor roles without emitting principal or role-definition ids. AzureSecretConfigProbe checks required Key Vault references by status only, never reads a response body or secret value, and emits hashed references. Reports record sanitized per-category check coverage even when clear. The private runner requires all four Azure categories in enforcement mode, combines them with bounded TLS egress evidence, stores only sanitized reports in private Blob storage, and binds both evidence digests into exact-plan verification. The Firewall / NSG topology adapter remains a separate future enhancement; it is not required for direct runner reachability evidence.
  2. Capability-mode toggle scaffold (shipped): infra/modules/preflight-toggles/ and the disk reference consumer validate both renderings. The generic upstream root does not instantiate the concrete consumer because resource ownership and integration stay fork-owned.
  3. Check-publishing seam (partly shipped): the core function, provider Protocol, in-memory publisher, and sanitized GitHub Checks adapter exist. Runtime binding after infrastructure-PR creation remains planned.
  4. Deployment Environment Profile refresh (shipped as a bounded task): the existing in-memory primitive now has a delivery-owned durable task using the shared state store. The recovery Inventory-delta job invalidates profiles before cursor advance; callers can inject a read-only builder and run a due refresh with expiry and restart recovery. No production profile builder or scheduler is bound, and a profile does not replace the fresh pre-publication gate.
  5. Control-loop pre-PR gate (partly shipped): pre_publication_gate.py re-runs the same analyzer on the accumulated overrides immediately before publication and withholds every unproven case - a blocking detected issue, stale evidence, a scope change, or an escalated reassembly - so the pass routes to hil and submits nothing. The PR delivery wrapper also holds on invalidated exact-patch evidence, but neither is bound to a live executor path yet.
To learn aboutRead
Delivery status and remaining workImplementation ledger