Skip to content

Preflight Active Plan Reassembly (policy blocker to re-rendered terraform)

Use this design to understand how the shipped pure loop responds when deployment-preflight reports a policy_guardrail or supply_chain_egress blocker that has a registered capability-mode toggle. The loop can calculate and re-verify overrides that actively re-render the terraform plan into a supported alternate shape. That shape never emits the denied operation. Delivery as a fix PR through the existing executor begins after live composition wiring lands.

This document is authoritative for the active-reassembly loop, its convergence and stop-conditions, the ActionType that carries it, and the honest limits of what can be reassembled. The blocker taxonomy, the toggle mapping table, and the report shape stay in deployment-preflight.md; the toggle modules themselves live in infra/modules/preflight-toggles/.

Customer-agnostic: no denylist value, mirror endpoint, or toggle default is baked in upstream. The upstream ships the reassembly machinery and the generic toggle catalog; a fork supplies the specific guardrail values and consumer wiring (generic-scope.instructions.md).

The rails already exist; active reassembly connects them end to end:

  1. Detection - a FeasibilityProbe emits a grounded ProbeFinding (feasibility_probe.py).
  2. Mapping - the detected issue carries a ProbeResolution(kind=TERRAFORM_TOGGLE, autofix, module, set_vars) naming the exact infra sub-module and the variable override that makes the deploy comply.
  3. Alternate rendering - the preflight-toggles modules encode the compliant shape (disk_provisioning=attach_existing, registry_source=acr_mirror, …) as data-only Terraform.

The two pieces this design added now have these states:

  • Toggle proposal builder (shipped): renders every autofix toggle in a cleared outcome as one typed proposal and submits it only behind the pre-publication gate. The live publisher binding is still absent, so it does not open a PR (check_publish.py).
  • Convergence loop (shipped): uses caller-provided plan-render and reanalysis callbacks to ensure a fix for one blocker cannot silently introduce another.

Reassembly is a bounded, deterministic loop, never a single shot - a re-rendered plan must be re-checked because a toggle can move a blocker rather than remove it.

terraform plan (JSON)
-> preflight.analyze
-> CLEAR -> deliver plan / merge
-> BLOCKED + autofix toggle for every blocking finding
-> render tfvars override (reassemble)
-> re-plan -> back to preflight.analyze (bounded)
-> BLOCKED + a blocking finding has no autofix toggle
-> hil (partial autofix is never applied)
  • All-or-nothing per pass: reassembly proceeds only when every blocking detected issue has an autofix toggle. A single manual-resolution blocker routes the whole pass to hil - the loop never applies a partial fix that would still fail apply.
  • Verifier is authority: the reassembled plan is re-checked by the same deterministic preflight (OPA re-check + what-if), never trusted because a toggle was applied. This mirrors the quality-gate rule: execution eligibility is granted by verification, not by the fix generator.

The loop MUST terminate. Its stop-conditions are safety invariants, not optimizations:

Stop-conditionEffect
max_reassembly_iterations (default 3) exceededroute to hil, attach the last report
same toggle proposed twice for the same detected issue idnon-convergence -> hil (prevents flip-flop / infinite loop)
a reassembly pass produces more blocking detected issues than the prior passregression -> hil
any probe raisesfail-closed -> hil (never reassemble on a partial pass)

The iteration counter, the per-detected issue toggle history, and the caps are configuration, not hardcoded literals, so a fork can tune them without editing core/.

ActionType: remediate.apply-preflight-toggle

Section titled “ActionType: remediate.apply-preflight-toggle”

Active reassembly is not a new privileged path. It reuses the existing executor by registering a first-class ontology ActionType, so the seven safeguards, observation-first gating, and the append-only audit entry come for free (the same reason the console vocabulary routes every action through the typed pipeline, see architecture.instructions.md).

The declaration (authored under rule-catalog/action-types/):

  • category: remediation
  • trigger_kind: both - the preflight loop initiates it automatically on a blocking detected issue, and an operator MAY request a specific toggle; it is parametric (argument_schema: scope, finding_id, toggle_module, set_vars, reason), so it is not a static resource-posture rule.
  • execution_path: pr_native - the change is a tfvars-override PR against the infra repo, never a direct substrate mutation.
  • rollback_contract: pr_revert - reverting the PR restores the prior tfvars; the reassembly is fully reversible, so irreversible: false.
  • default_mode: shadow - the first ship judges and renders the PR as a draft with the shadow label; it never auto-merges.
  • promotion_gate - measured on the frozen scenario set (false-positive rate of the toggle mapping) before any per-category promotion to enforcement mode.
  • preconditions - graph_fresh_within_seconds (the plan and the environment profile must be current) and no_conflicting_open_action_on_resource.
  • stop_conditions - the convergence caps above, plus the standard time_box_exceeded_seconds and provider_api_error_streak.
  • blast_radius - the set of infra variables the override touches; a reassembly that would flip more toggles than the cap holds for review to hil.

An autofix PR is proposed automatically only when all of these hold; otherwise the detected issue degrades to guidance + hil:

  1. the resolution kind is TERRAFORM_TOGGLE with autofix: true;
  2. the toggle is a deterministic data-only module (no LLM in the path);
  3. the reassembled plan re-passes preflight (verifier re-check);
  4. the override stays within the declared blast_radius.

autofix: false toggles submit no proposal or diff. They remain manual guidance in the report, the whole pass escalates, and the operator reviews the variable change.

A cleared loop is a decision, not an authorization. The pre-publication gate (pre_publication_gate.py) re-runs the same analyzer on the accumulated overrides immediately before the proposals are submitted, and withholds the whole pass when the fresh report is blocked, stale, bound to another scope, or when the loop escalated. Every hold is decided before the first submission, so a withheld pass submits nothing - never a partial proposal set - and the caller routes it to hil (publication holds).

Submission crosses the ingress boundary as preflight_toggle_blocker, a control-plane signal rather than an operator request. Huginn honors a payload-supplied action_type only for an explicit operator request, so this signal carries none: Forseti binds remediate.apply-preflight-toggle from its own event-type table and issues the default hil decision. A blocker therefore cannot reach a provider commit without a distinct human approval, and no ingress producer can spoof the ActionType. The same trust gate drops params for this signal, so the composed live path still owes a governed way to carry the per-toggle arguments to the executor.

A reassembly can apply several toggles (across detected issues and iterations). Each applied toggle becomes its own remediate.apply-preflight-toggle Action - not one bundled Action per pass. This keeps the ActionType’s argument_schema single-toggle (finding_id + toggle_module + set_vars), so audit, rollback (pr_revert), and impact scope stay at toggle granularity and map 1:1 to the detected issue each toggle resolves. The loop retains the per-toggle provenance (AppliedToggle: finding_id, module, set_vars, scope); the proposal builder (reassembly_proposals.py) renders one proposal per toggle and submits each through the same typed pipeline seam an operator command re-enters (ProposalSink -> Huginn -> Forseti -> Thor), observation-first. An escalated outcome yields no proposals - the caller routes it to hil.

Honesty about the boundary is a safety property, not a caveat:

  • Reassemblable - blockers with a registered alternate rendering: inline disk deny -> attach_existing; blocked docker.io egress -> acr_mirror; NSG create deny -> byo; PyPI egress deny -> internal python_index_url; ordering violation -> dependency_ordering=strict.
  • Not reassemblable (routes to hil) - policies with no supported alternate: a region banned outright, a mandatory-tag policy, a denied SKU with no substitute SKU, or any guardrail whose only resolution is a scoped exemption or a governance decision. These emit a MANUAL resolution and never auto-reassemble.

The preflight composition can report a recurring MANUAL blocker to Norns with a bounded detected issue id, category, evidence source, and scope. Norns deduplicates scope digests and, after three distinct scopes by default, proposes one inert preflight-toggle-gap candidate. The candidate suggests a new reviewed alternate rendering and enters the standard quality gate; it never creates a toggle or raises deployment authority (architecture.instructions.md § Rule Catalog).

Every reassembly action satisfies all seven safeguards, enforced by the executor it reuses:

  • Stop-condition - the convergence caps above, declared on the ActionType.
  • Rollback path - pr_revert; the override PR is a single-commit revert away from the prior plan, and the rollback reference is embedded in the PR body.
  • Impact scope limit - the reassembly touches only the declared infra variables; exceeding the cap holds for review to hil.
  • Audit-log entry - the pure loop returns an audit-grade terminal reason and toggle provenance. Live composition writes the hash-chained audit record when it submits the result; the currently unwired core primitive does not call the audit store.

Reassembly ships observation-first: the PR is a draft, judged and rendered but not merged, until the toggle mapping’s false-positive rate is measured and the category is explicitly promoted to enforcement mode.

PieceLocationStatus
Toggle resolution on a detected issuefeasibility_probe.pyshipped
Capability-mode toggle modulesinfra/modules/preflight-toggles/shipped (data-only)
Readiness report + decisioncore/deploy_preflight/report.pyshipped
Report -> PR check publishcore/deploy_preflight/check_publish.pyshipped (report only)
Convergence loop + stop-conditionscore/deploy_preflight/reassemble.pyshipped
remediate.apply-preflight-toggle ActionTyperule-catalog/action-types/shipped
Overrides -> Action proposals (one per toggle)core/deploy_preflight/reassembly_proposals.pyshipped
Re-verification before publicationcore/deploy_preflight/pre_publication_gate.pyshipped
Recurring manual blocker -> inert candidateagents/norns.pyshipped (caller-supplied observation)
Reference consumer wiring (one toggle)infra/modules/preflight-toggles/reference-disk-consumer/shipped (fork copies it)
Composition wiring: ProposalSink + live triggercomposition root + delivery/azure/preflight/remaining

core/ sees only the FeasibilityProbe Protocol and a caller-supplied ProposalSink callable; the reassembly loop constructs no cloud SDK and opens no PR itself - it decides the overrides and hands them to the executor (via the ActionType), which owns the publish and the invariants.

Each is separately reviewable:

  1. Docs-first (this document) - the loop, ActionType, and limits. (shipped)
  2. The remediate.apply-preflight-toggle ActionType YAML + schema validation. (shipped)
  3. The bounded convergence loop, observation mode, with property tests: “same toggle never applied twice”, “partial blocker -> human approval”, “reassembled plan is re-verified”, “regression -> human approval”, “fail-closed on a raising reanalyze”. (shipped)
  4. One reference consumer wiring (the disk_provisioning toggle) under infra/ so a fork has a copy-paste starting point. (shipped)
  5. The overrides-to-executor step: render each applied toggle into a remediate.apply-preflight-toggle proposal (one Action per toggle, granularity A) and submit through the typed pipeline seam. (shipped)
  6. Composition wiring (bind the gated ProposalSink to Huginn ingest at the runtime composition root) plus live Azure adapters that feed real policy detected issues into the loop and open the tfvars-override PR (after the preflight live adapters land, observation-first). The gated seam and its Huginn/Forseti integration test exist; the composition root and live trigger are (remaining).
To learn aboutRead
Delivery status and remaining workImplementation ledger