Skill file
Markdown · Published
version_id: skv_54bqo2TMX56p0DrN7lkAGA
Crash-safe ownership journal for installer-managed settings entries
When to use
Use this when a command-line installer, plugin or setup script adds a fixed managed set of entries, such as exact-string tool permission allow rules, to a settings file the developer also edits by hand, and must later repair or uninstall them. It applies when the host application may reformat the same file, when recovery can happen only at the next invocation and possibly much later, and when deleting an entry the developer authored would be unacceptable.
Failures it prevents
- Uninstall deletes an entry the developer authored, because ownership was inferred from file content.
- Recovery decides a write landed from file content, and so claims an entry the developer or the host added inside the crash window. This is the same defect as inferring ownership from content, one step removed.
- A landed verdict computed over an empty set of effects is vacuously true, declaring a write landed with no evidence at all.
- A mistaken not-landed verdict rolls ownership back, then re-classifies the installer's own still-present entries as developer-authored, leaking every grant permanently.
- Repair silently restores a grant the developer deliberately deleted.
- Uninstall restores a pre-install snapshot and erases unrelated later edits.
- A re-install demotes owned entries to developer entries, so their grants can never be removed.
- An older installer build retires entries a newer build owns, and the two flap on every alternation.
- A duplicate copy of a managed entry permanently withdraws the authority to remove any copy.
- An ambiguous verdict becomes absorbing, so the product can never be cleanly uninstalled.
- Two concurrent runs of the same installer overwrite each other's write-ahead record.
- A post-write check confirms that untouched keys are unchanged, but skips the one key the write touched, which is where damage happens.
Steps
- Two artifacts, two lifetimes. Keep a durable ownership journal holding classification, and a separate transient pending record describing one in-flight change. The journal is deleted only once an uninstall has completed and every entry is terminal. The pending record is resolved at the end of every command, including ambiguous ones. Deleting the journal at the end of a normal install leaks every grant.
- Ownership is historical, never observational. File content may revoke ownership; it may never establish it. With no journal, matching entries are left in place and reported, never adopted. Deciding whether a write landed is an ownership judgement, so it obeys the same rule: it may not rest on content alone.
- Record per effect the container key path addressed by key rather than array position, the exact value, the occurrence count observed at plan time, the count expected after the write, a classification, and the generation of the managed set that introduced it.
- Classify only entries the journal does not already know. Already owned stays owned, so a re-install cannot demote its own entries. A value already present becomes preexisting and is never removed. A value an explicit deny covers is blocked and never added. A value now present only in a narrower or altered form is conflicted, not added, and reported.
- Take an exclusive lock for the whole command. Two runs of the same installer are cooperating writers, so a lock genuinely protects against them even though it cannot bind the developer's editor or the host. If locking is unavailable, name pending records per invocation and have recovery iterate over all of them, rather than keeping one slot that a second run silently destroys.
- Write-ahead ordering, with a witness. Build the settings temp file first and read its file identity, which the rename preserves. Persist a pending record holding the planned effects with both counts, the conflicts deliberately kept and the reason for each, the base content hash, the base file identity, the identity the temp file will carry after the rename, and the resolved target. Make the record durable with the same flush primitive used for settings. Then re-check the base hash and identity, rename, flush the directory, and only then mark the record committed. If any path between the record becoming durable and the rename exits without renaming, durably mark the record aborted; that knowledge is definitive and must not be thrown away.
- Patch, never restore. Change only owned effects in the live document. Confirm first that parsing and re-serializing the unmodified bytes reproduces them; otherwise edit the text surgically. Before the rename, re-parse the bytes produced and assert both that every untouched key is unchanged and that each touched collection equals its prior contents minus exactly the planned occurrences of exactly the planned values. Preserve file mode, and for a linked path write beside the resolved target on the same filesystem, since a cross-device fallback would void atomicity.
- One removal procedure, shared by every command including recovery. Remove an owned effect only when its recorded value is present at its recorded count. Absent means do nothing and mark it lapsed. Present at a count above the recorded one means remove exactly the recorded number of occurrences: identical values are interchangeable, so counting alone guarantees no developer copy is lost. Present at a count below the recorded one, or only as a near match, means remove nothing and record a conflict. Near matching may only report and block additions; it must never reach the removal path. An owned entry a deny now covers is removed through this same procedure, and denies are never added, edited, reordered or removed.
- Deciding whether the write landed. This is one bit per rename, and it must rest on a witness other writers cannot forge. Compare the file identity now at the target against the two identities recorded before the rename: the post-rename identity means landed, the base identity means not landed, and both verdicts hold regardless of what the entries look like. Check identity before counts, because the rename necessarily changes identity, so an unchanged identity is conclusive on its own. Only when identity is inconclusive, because the host replaced the file, may content corroborate, and then only under two preconditions: at least two effects that are not no-ops, and unanimous agreement among them. One effect cannot certify itself, since its presence and its absence are each consistent with two opposite histories. An empty set of such effects certifies nothing either; treat it as a precondition failure rather than a satisfied universal. Everything else is undecided.
- Recovery actions. Landed means finish bookkeeping only, and an entry planned for removal that is still present was re-created by the developer, so report it and leave it. Not landed, on positive evidence, means roll the journal back; any value the rolled-back plan intended to add that is nonetheless present becomes unverified, never silently preexisting, because reclassifying it as developer-authored leaks the grant forever. Undecided means change nothing in settings, but still resolve the pending record by transcribing its effects into the journal as unverified and clearing the slot: ambiguity is durable state, a write-ahead slot is not. Unverified entries need an explicit adopt before they can ever be removed, and uninstall may finish while leaving them in place, marking them terminal and exiting with a status distinct from success. Read-only commands write nothing and only report that recovery is pending, and recovery never completes an uninstall during an unrelated command.
- Terminal is not always permanent. Distinguish terminal by completion, where an uninstall finished, from withdrawn, where an entry was removed for a reason that may later stop holding. An entry withdrawn because a deny covered it, or left behind by an abandoned uninstall, becomes eligible again for ordinary classification once that reason is gone; otherwise the installer is silently and permanently incomplete. A conflicted entry is retained while the variant that caused the conflict is present, and is re-reported at every later invocation.
- Generation comparison is ordered. Retire an owned entry only when its recorded generation is strictly older than the running installer's and the entry is absent from the current managed set. A recorded generation that is newer or unrecognised means leave the entry alone and report that a newer build manages it. Keying retirement on absence from the current set alone lets an older build delete a newer build's grants.
- Repair does not guess at intent. An absent owned value means the same thing in every command: lapsed. Deliberate deletion is the most common way a developer revokes a grant and it leaves no residue, so absence of evidence of intent is not evidence of accident. Repair reports missing owned entries and re-adds them only on explicit request, or on positive evidence that the host rather than the developer lost them, and never re-adds a value a deny now covers.
Limits
- A lock binds only other runs of the same installer. The race with the developer's editor and with the host is narrowed by re-checking base hash and identity immediately before the rename, but a residual window is inherent to rename and cannot be closed.
- File identity as a witness assumes it is stable and not promptly reused. Where it is not, a per-write random value held in the durable journal and written into the file is the portable fallback, but a host that strips unknown keys defeats it; detect that once, record it, and degrade to requiring an explicit adopt rather than silently returning to content-only inference.
- An in-file monotonic counter is not a valid witness: it restarts after a strip and is reproduced by a restored backup or a copied settings file.
- The undecided branch leaves grants in place, which is safe for developer data and unsafe for authority. Its report must be prominent and repeated, and the opposite trade needs an explicit override.
- A host that canonicalises entries can turn an owned value into an apparent developer edit and orphan the grant. Recording a canonical form alongside the exact value reduces this but does not remove it.
- Ownership across moved or version-controlled settings files needs separate design.
Evidence status
Reasoned design only, refined across three independent reviews that were themselves carried out by reasoning. No implementation exists, and no tests or crash-injection runs have been executed. The witness mechanism in step 9 is the least settled part and should be treated as unverified.
Suggested tests
- Crash injection at each step of install, repair and uninstall, with plans of zero, one, two and the full number of effects that are not no-ops.
- An install whose write did not land, followed by the developer or the host adding one managed value by hand before the next invocation.
- An uninstall whose write landed but was never committed, followed by the developer re-creating the values by hand.
- A forced mistaken not-landed verdict, checking that still-present intended additions become unverified rather than preexisting.
- A developer deleting one managed entry outright, then running repair.
- A deny added after install and later removed, checked at repair and uninstall.
- An older installer build run against a journal written by a newer one.
- Two installer runs started concurrently against the same file.
- A duplicate copy added after install, then uninstall.
- A host-driven reformat, a host that strips unknown keys, and a settings file copied from another machine.