Found by the adversarial review lane of the #363/#368/#378 quorum (branch fix/plan-apply-integrity).
What
forjar apply runs check_pre_apply_drift (a live probe of every host, which writes ResourceStatus::Drifted into the lock) before converging. forjar apply --plan-file p.json does not. The omission is deliberate and documented at src/cli/apply_from_plan_gates.rs:83: a gate must not mutate state a later gate may refuse. The saved plan is bound to config_hash and state_hash instead, so a plan whose STATE FILE moved is refused — that is Terraform's apply tfplan shape (lineage/serial binding, no refresh) — but a host that drifted UNDER an unchanged state file is converged over without a word.
Why it matters
The interactive apply refuses on live drift; the saved plan is therefore a way to get past that refusal without saying so. The review's claim "every gate the interactive apply runs also runs on --plan-file, or is refused by name" was REFUTED on this point and recorded as such in the branch's receipt.
Fix options (pick one, cite it)
- A read-only drift probe on the
--plan-file path (no lock write), refusing by name — drift gate: <id> drifted since the plan was sealed; re-run forjar plan — unless --force.
- Keep the omission and print it: one line in the plan-file apply's preflight output,
drift gate: not run for saved plans (bound to state_hash instead), so nobody mistakes the binding for a probe.
Option 2 is the minimum honest change; option 1 matches Pulumi's up --plan (live evaluation at execution time).
Falsification test
Seal a plan, tamper a managed file on the host, apply --plan-file: today it converges silently. After the fix: refused by name (option 1) or the notice appears in the preflight output (option 2).
Refs #368 (the gate parity work that surfaced this).
Found by the adversarial review lane of the #363/#368/#378 quorum (branch fix/plan-apply-integrity).
What
forjar applyrunscheck_pre_apply_drift(a live probe of every host, which writesResourceStatus::Driftedinto the lock) before converging.forjar apply --plan-file p.jsondoes not. The omission is deliberate and documented atsrc/cli/apply_from_plan_gates.rs:83: a gate must not mutate state a later gate may refuse. The saved plan is bound toconfig_hashandstate_hashinstead, so a plan whose STATE FILE moved is refused — that is Terraform'sapply tfplanshape (lineage/serial binding, no refresh) — but a host that drifted UNDER an unchanged state file is converged over without a word.Why it matters
The interactive apply refuses on live drift; the saved plan is therefore a way to get past that refusal without saying so. The review's claim "every gate the interactive apply runs also runs on --plan-file, or is refused by name" was REFUTED on this point and recorded as such in the branch's receipt.
Fix options (pick one, cite it)
--plan-filepath (no lock write), refusing by name —drift gate: <id> drifted since the plan was sealed; re-run forjar plan— unless--force.drift gate: not run for saved plans (bound to state_hash instead), so nobody mistakes the binding for a probe.Option 2 is the minimum honest change; option 1 matches Pulumi's
up --plan(live evaluation at execution time).Falsification test
Seal a plan, tamper a managed file on the host,
apply --plan-file: today it converges silently. After the fix: refused by name (option 1) or the notice appears in the preflight output (option 2).Refs #368 (the gate parity work that surfaced this).