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).
Where It Sits in the Loop
Section titled “Where It Sits in the Loop”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
hilrather 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.
Probe Taxonomy
Section titled “Probe Taxonomy”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:
| Category | Representative blocker | Detection (deterministic-first) |
|---|---|---|
policy_guardrail | disallowed resource types, NSG required, inline disk denied, public IP denied | terraform plan JSON re-checked against policies/ (OPA) + Azure Policy deny simulation (static) |
supply_chain_egress | docker.io blocked, PyPI / npm / apt blocked, external base image pull denied | NSG / Firewall / UDR rule analysis (static) + bounded egress reachability probe (live) |
identity_rbac | executor identity lacks a role on the target scope; cannot create a role assignment | scope role-assignment check from the inventory graph (static) |
quota_capacity | SKU / region quota exceeded, zone capacity unavailable | quota lookup (live, cached) |
dependency_ordering | disk before VM, NSG before subnet, private endpoint before resource | ordering violation derived from policy + the module dependency graph (static) |
secret_config | Key Vault reference unresolvable, required secret absent | secret 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.
Readiness Report
Section titled “Readiness Report”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) orwarning(surfaces but never gates). - resolution - how to clear it, mapped to a concrete lever when possible (see the toggle table below).
Decision Semantics
Section titled “Decision Semantics”| Decision | Meaning |
|---|---|
clear | no detected issues |
needs_review | detected issues exist but none is blocking (warnings only) |
blocked | at 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.
Observation-first
Section titled “Observation-first”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.
Publication Holds
Section titled “Publication Holds”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:
| Hold | Cause |
|---|---|
no_applied_toggle | the loop applied no toggle; the pass is a no-op rather than a rejection |
reassembly_escalated | the loop escalated; a partial reassembly is never submitted |
blocking_finding | the re-verified report still carries a blocking detected issue |
stale_evidence | the report is outside the freshness window or its timestamp is unusable |
scope_drift | the 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.
Blocker to Terraform Toggle Mapping
Section titled “Blocker to Terraform Toggle Mapping”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:
| Toggle | Values | Effect |
|---|---|---|
disk_provisioning | inline | attach_existing | create the VM disk inline vs attach a pre-provisioned disk (var.existing_disk_ids) |
nsg_provisioning | create | byo | create an NSG vs reference an existing one (var.existing_nsg_id), attached as the guardrail requires |
registry_source | docker_io | acr_mirror | pull 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_ordering | strict | split 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.
Subsystem Layout
Section titled “Subsystem Layout”| Piece | Location | Role |
|---|---|---|
| Probe seam | shared/providers/feasibility_probe.py | FeasibilityProbe Protocol + detected issue / target dataclasses |
| Generic probes | shared/providers/local/feasibility.py | deterministic, config-driven upstream defaults (no network) |
| Orchestrator | core/deploy_preflight/analyzer.py | fan out over probes, assemble the report (fail-closed) |
| Report | core/deploy_preflight/report.py | the assembled artifact + decision + blocks_deploy |
| Pre-publication gate | core/deploy_preflight/pre_publication_gate.py | re-verify before proposal submission; withhold and route to human review |
| PR delivery wrapper | delivery/deploy_preflight/pr_publication.py | require an exact-patch refresh before the injected PR publisher |
| GitHub Checks adapter | delivery/github/preflight_checks.py | post 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/.
Safety Posture
Section titled “Safety Posture”- Fail-closed - a probe that raises propagates; the pass never reports
clearon a partial run. A blocking detected issue degrades a control-plane action tohil, 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).
Delivery Status
Section titled “Delivery Status”Shipped: the probe seam, generic deterministic probes, analyzer + report, standalone Azure preflight script, protected-plan evidence binding in the deploy workflow, and tests.
- Azure probes and protected-plan evidence (shipped): a shared read-only ARM client (
AzureArmClient, injectedhttpx.AsyncClient+WorkloadIdentitybearer token, fail-closed) plus theAzurePolicyGuardrailProbe(real Azure Policydenyguardrails -Not allowed/Allowed resource types) and theAzureQuotaProbe(Compute usages per subscription + location) have landed with mock-HTTP unit tests. The policy parser accepts the built-inallOftype 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 todisk_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.pycomposes 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 throughAzureIdentityRbacProbeto report missing event-bus and secret-reader executor roles without emitting principal or role-definition ids.AzureSecretConfigProbechecks 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. - 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. - 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.
- 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.
- Control-loop pre-PR gate (partly shipped):
pre_publication_gate.pyre-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 tohiland submits nothing. The PR delivery wrapper also holds on invalidated exact-patch evidence, but neither is bound to a live executor path yet.
References
Section titled “References”- architecture.instructions.md - control loop, quality gate, safety invariants
- project-structure.md - module boundaries, infra sub-module pattern
- risk-classification.md - how a blocking detected issue routes to
hil - rule-catalog-collection.md - where the underlying guardrail rules come from
Related docs
Section titled “Related docs”| To learn about | Read |
|---|---|
| Delivery status and remaining work | Implementation ledger |