Ownership journal for installer-managed allow rules: crash-safe install, repair and uninstall
How a CLI installer that inserts a fixed set of exact allow rules into a shared settings file should journal ownership so repair and uninstall preserve user allows, denies and unrelated edits, remove only still-matching owned effects, report conflicts, and recover from crashes without ever deleting a developer rule or claiming one. Reasoned design plus adversarial review, not executed tests.
Ownership journal for installer-managed allow rules
When to use
Use this when a CLI installer adds a small fixed set of exact permission allow rules to a settings file that the developer also edits by hand and that may contain explicit deny rules. The installer must offer repair and uninstall that never remove a rule the user wrote, never disturb unrelated settings, and never add, remove or bypass a deny. The settings write and the ownership journal cannot share one transaction.
This is a reasoned design, corrected after an independent adversarial review. No executed tests back it.
Effects model
The installer owns effects, not the file. An effect is one of:
- an allow list entry identified by target file, exact rule string and the number of instances the installer inserted, which is zero or one because the installer never inserts a string that is already present;
- the installer's own namespaced witness entry in that file;
- optionally, a container the installer created, such as the file itself or an empty allow list, recorded so uninstall can remove it only if it is still empty.
Nothing else in the file is ever read for ownership or written. Every command is a structural edit: parse, change only owned effects, serialize. Never restore a pre-install snapshot; it would overwrite every unrelated edit made since.
Invariant
The journal must always claim at least as much as the installer has placed in settings, and every command runs recovery first. The invariant bounds what the journal claims. It says nothing about who wrote a string that is present now, so it never justifies removing a rule whose provenance is uncertain.
- Install: journal a pending intent first, settings second, journal confirmation to owned third.
- Uninstall: journal a releasing intent first, settings shrink second, journal released third, journal deletion last.
Landing evidence
At intent time the installer already holds the parsed file and the plan, so it can serialize the exact bytes it will write. Record three things in the intent: the pre-write digest, the predicted post-write digest, and a fresh random marker that the write places in the witness entry. Recovery compares the current file digest:
- equal to the predicted digest: the write landed.
- equal to the pre-write digest: the write never happened.
- anything else: another writer touched the file. Use marker equality as a secondary hint, and resolve any remaining doubt toward touching nothing and reporting.
Marker presence alone is never evidence. An older marker from a previous attempt is always present after the first install, and a host application or editor may strip an unknown entry. Only equality with the marker of this specific intent counts.
Journal records
One record per target file and exact rule string, plus one witness record per target file. States: pending, owned, preexisting, vetoed, releasing, released. Preexisting rules were present before install and are never removed or re-added. Vetoed rules had a matching deny at install and were not inserted. Released rules were removed by uninstall or were tombstoned after being observed absent, with a conflict note. Records also hold installer version, managed rule set, insert count, and the digests and marker of the current intent.
Write the journal with a temporary file, flush, and atomic rename. An unparseable journal fails closed: refuse repair and uninstall, report, and require an explicit force flag whose only permitted action is removing exact managed strings with a loud report, since preexisting status is unknown. Canonicalize target paths, resolving symlinks and case differences, so a second install cannot create duplicate ownership.
Recovery, run first by every command
Pending records:
- landed by digest, or witness marker equals this intent: promote to owned.
- not landed by digest: discard the intent.
- file changed by another writer, rule absent: tombstone as released with a conflict note. Do not discard silently, or repair would treat the rule as new and re-grant it.
- file changed by another writer, rule present, marker not equal: ambiguous. Stay pending, report, and require the developer to claim or disclaim explicitly. Uninstall leaves such a rule and reports it.
Releasing records:
- landed by digest: mark released.
- not landed by digest: rebuild the plan from the current file and retry.
- file changed by another writer, rule absent: mark released.
- file changed by another writer, rule present, witness marker equals the recorded one: the shrink did not happen; rebuild the plan and retry.
- file changed by another writer, rule present, witness absent or different: the shrink may have landed and the developer may have re-added the rule. Do not remove it. Mark released with the note possibly re-added by developer.
Owned record, rule absent: the developer removed or edited it by hand. Tombstone as released. Install that meets releasing records completes the release first, then proceeds with a fresh intent.
Recovery is per record and per target file. A crash between two target files leaves one file released and the other releasing; each resolves independently, and the journal is deleted only when no owned, pending or releasing record remains anywhere, including the witness records.
Deny handling
- A deny that exactly matches a managed rule at install time vetoes insertion. Record vetoed and tell the user. A broader deny pattern does not veto; the allow is inserted and shadowed, which is consistent with never touching denies.
- A deny added later for an owned rule blocks repair from re-adding; uninstall still removes the installer allow.
- The installer never adds, removes or rewrites a deny under any command.
Repair
Repair requires a journal; with none, report not installed rather than behaving as install. Run recovery, then per record:
- owned and present: nothing.
- owned and absent: already tombstoned. Re-grant only on explicit confirmation or reinstall. Silently restoring a permission the developer removed is a security surprise.
- preexisting and absent: nothing.
- vetoed with the deny now gone: fresh intent, insert, promote to owned.
- rules newly managed in this version: fresh intent, insert.
- rules no longer managed but still owned: remove as in uninstall, but rewrite the witness with a fresh marker instead of removing it while any other rule stays owned.
Uninstall: compare before remove
- Take a lock that spans the whole command, from recovery through the final journal write. Parse the current file; if it is empty or unparseable, refuse to write.
- Build the plan from the current parse. For each owned record remove the smaller of the recorded insert count and the current exact-match count; zero means modified or removed by the developer, so plan no change and report a conflict. Always plan removal of the witness entry if its value equals the recorded marker, independent of rule records. Plan removal of a recorded container only if it is still empty.
- Write the releasing intent with the plan, pre-write digest, predicted digest and marker in one atomic journal write.
- Apply the plan to the parsed structure and touch nothing else.
- Self-check: the semantic difference between the old and new parse must equal the plan exactly, else abort. Re-read and abort if the digest changed since step one. Write with temporary file, flush of file and directory, atomic rename.
- Mark records released. Delete the journal only when every record, including the witness record, is released, preexisting or vetoed.
Matching is exact string equality only. Fuzzy or prefix matching is rejected because any similarity rule can be satisfied by a user rule. An in-place edit of an owned rule is indistinguishable from delete plus user add, and exact matching classifies it correctly.
Conflict report
One line per record with a fixed category: removed; left, modified or removed by developer; left, possibly re-added by developer; left, ambiguous ownership; left, preexisting; left, extra duplicates; left, deny present; left, witness changed; left, found in a different scope file. Offer a dry-run flag that prints the plan and report without writing. Use a format-preserving editor when the format supports comments, and warn once that re-serialization may normalize formatting.
Known limits
- A rule the developer removed and re-added by hand between commands is indistinguishable from untouched and will be removed by uninstall. Document it.
- If the host application deduplicates identical strings, a developer duplicate of an owned rule collapses and uninstall removes the survivor.
- The window between the final re-read and the rename is inherent; the lock narrows it against the installer's own commands only.
Anti-patterns
- Treating witness presence or absence as proof of what the installer did.
- Removing a present rule because the journal claims it, when another writer has touched the file since the intent.
- Silently discarding an intent when the file changed, so repair re-grants a removed rule.
- Tracking the witness only through rule records, so it is orphaned when every rule is tombstoned.
- Locking per write instead of per command.
- Restoring a pre-install snapshot on uninstall.
- Removing a rule that merely resembles an owned rule.
- Guessing ownership when the journal is unparseable.
- Touching deny lists for any reason.
Supporting basis and limitations
Derived by reasoning about crash points on both sides of the settings write for install and uninstall, during the journal write, and between target files, then checked by an independent adversarial review that produced concrete failing scenarios for the earlier recovery table. Every failure traced to inferring landing from witness presence rather than from a predicted digest and marker equality, or to resolving ambiguity in the direction that removes or claims a rule. No tests were executed.