Skip to content

Deployment Preflight (feasibility and blocker collection)

Before a deployment runs (terraform apply, or a control-plane fix PR), the deploy-preflight pass 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).

AreaStateEvidenceNotes
Probe contracts, deterministic probes, analyzer, and reportimplementedservices/core-control-plane/src/fdai/core/deploy_preflight/, services/core-control-plane/src/fdai/shared/providers/feasibility_probe.py, and focused deploy-preflight testsStable detected issues, fail-closed probe execution, decisions, and observation mode-versus-enforce behavior are tested.
Read-only Azure probes and protected-plan evidenceimplementedscripts/deployment/azure/run_live_preflight.py, .github/workflows/deploy-dev.yml, and tests/integration/scripts/test_run_live_preflight.pyThe protected runner invokes the standalone script, requires all four live categories, sanitizes evidence, and binds its digest to the plan.
Terraform toggle and environment-profile primitivesimplementedinfra/modules/preflight-toggles/ and focused test_environment_profile.py and test_reassembly_proposals.py checksThe root app-graph consumer and durable profile refresh task are not composed.
Check publishing primitiveimplementedservices/core-control-plane/src/fdai/core/deploy_preflight/check_publish.py and test_check_publish.pyThe pure report publisher and in-memory adapter are tested; there is no GitHub Checks adapter.
Control-loop pre-PR gate and GitHub deliverynot-startedThe planned boundaries in this documentNo live path invokes the analyzer before a fix PR or publishes the result to GitHub Checks.
DateStateChangeEvidenceRemaining
2026-08-14in-progressAdopted the implementation ledger; earlier provenance was not reconstructed. Corrected the protected-runner path to the current standalone preflight entrypoint.current change; focused core preflight and live-script checks listed in the scope tableCompose the root toggle consumer, durable profile refresh, GitHub publisher, and control-loop gate.
  • Bind the root app graph to supported preflight toggles and pass a focused Terraform test that proves the denied shape is absent from the re-rendered plan.
  • Add a durable environment-profile refresh task with Inventory-delta invalidation and pass restart and expiry tests.
  • Invoke the analyzer before fix-PR publication, lower blocking detected issues to human review, and prove with an integration test that no PR opens on a blocked report.
  • Publish the sanitized report through a GitHub Checks adapter and retain a focused contract test for redaction and failed delivery.

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 (planned): 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.
  • 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 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

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 the contracts. Root app-graph consumer wiring is planned.
  3. Check-publishing primitive (shipped): the core function, provider Protocol, and in-memory publisher exist. The GitHub Check adapter and infrastructure-PR wiring are planned.
  4. Deployment Environment Profile primitive (shipped): bounded in-memory cache, TTL, and Inventory-delta invalidation helper exist. The composition refresh task and durable cache wiring are planned.
  5. Control-loop pre-PR gate (planned): invoke the same analyzer before the executor creates a fix PR and lower blocking detected issues to hil on the live path.