fix(planner): a source: file's CONTENT is part of the desired state (Refs #206) - #207
Merged
Conversation
…Refs #206) `hash_desired_state` hashes resource FIELD STRINGS. For `content:` that is correct - the content IS the field. For `source:` the field is a PATH, so editing the referenced file left the hash identical, determine_present_action planned NoOp, and apply skipped the resource: $ echo VERSION-ONE > payload.txt && forjar apply --yes Apply complete: 1 converged, 0 unchanged. $ echo VERSION-TWO > payload.txt && forjar apply --yes Apply complete: 0 converged, 1 unchanged. $ cat /tmp/deployed # -> VERSION-ONE (stale) `--force` was the only workaround. For a tool whose entire contract is "converge to declared state", silently not converging while printing success is the worst available failure mode. Found live in paiml/infra PMAT-204: an edited reconciler script reported "converged" three times while the box kept executing the previous copy. That is the same class as the ENOSPC incident there, where a systemd unit was edited but never actually deployed. The planner now folds the content hash of the source file into the desired state, via canonical_source_content(), following the existing canonical_overlay_hosts() pattern - whose doc comment already states this exact invariant for a different field: two resources differing ONLY in that field MUST hash differently or plan will false-report NoOp. Care taken: * The component is APPENDED, never inserted. Field order is hash identity; a reorder would invalidate every recorded hash on every machine. * It is pushed ONLY when `source:` is present, so source-less resources keep their existing hash exactly. * The path is read as written, matching resources::file::source_file_base64 - both resolve against the process CWD - so the planner hashes precisely the bytes apply would upload. * An unreadable source folds its error in rather than hashing as absent, so a file appearing or disappearing changes the hash instead of staying pinned at "unchanged". Upgrade note: source-based file resources show one Update on the first apply after upgrading, as their hash gains the new component. That re-apply is convergent and expected. Verified end-to-end with the fixed binary: plan reports "update (state changed)", apply deploys the new bytes with NO --force, and a third apply returns to "unchanged" - the fixed point still holds. Tests: 5 new in tests_hash_source.rs (content change, determinism, path identity, source appearance, source-less non-regression). Full suite 12596 passed / 0 failed. Contract: contracts/source-content-identity-v1.yaml - the completeness leg of idempotent-apply-v1, which asserts "differing hash always plans Update": sound, but vacuous if the hash cannot differ when the deployed artifact differs. Deliberately declares NO Kani harness: the property quantifies over filesystem contents, which is outside Kani's model, so a harness could only prove something about a stubbed read. pv validate: 0 errors, 0 warnings. Closes #206 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #206.
The bug
hash_desired_statehashes resource field strings. Forcontent:that's correct — the content is the field. Forsource:the field is a path, so editing the referenced file left the hash identical,determine_present_actionplannedNoOp, and apply skipped the resource:--forcewas the only workaround. For a tool whose entire contract is "converge to declared state", silently not converging while printing success is the worst available failure mode.Found live in paiml/infra PMAT-204 — an edited reconciler script reported "converged" three times while the machine kept executing the previous copy. Same class as the ENOSPC incident there, where a systemd unit was edited but never actually deployed.
The fix
Fold the source file's content hash into the desired state via
canonical_source_content(), following the existingcanonical_overlay_hosts()pattern — whose doc comment already states this exact invariant for a different field:source:was violating the same rule.Care taken:
source:is present, so source-less resources keep their hash exactly.resources::file::source_file_base64— both resolve against the process CWD — so the planner hashes precisely the bytes apply would upload.Verification
End-to-end, with the installed fixed binary:
tests_hash_source.rs(RED-verified before the fix)cargo fmt --checkcargo clippypv validatenew contractContract
contracts/source-content-identity-v1.yaml— the completeness leg ofidempotent-apply-v1. That contract asserts "differing hash always plans Update": sound, but vacuous if the hash cannot differ when the deployed artifact differs.It deliberately declares no Kani harness. The property quantifies over filesystem contents, which is outside Kani's model — a harness could only prove something about a stubbed read, i.e. prove nothing about this bug. The 7 falsification tests discharge the obligations instead, including an end-to-end one that reproduces the reported defect.
Upgrade note
Source-based file resources will show one
Updateon the first apply after upgrading, as their hash gains the new component. That re-apply is convergent and expected.Released as 1.12.3.
🤖 Generated with Claude Code