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 ownership conflicts, and recover from crashes including partially completed uninstalls. Reasoned design, 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 record cannot share one transaction.
This is a reasoned design. No executed tests back it.
Effects model
The installer owns effects, not the file. An effect is either one allow list entry identified by target file plus exact rule string plus the number of instances the installer inserted, or the installer's own namespaced witness entry. Nothing else in the file is ever read for ownership or written by the installer. 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 before acting. Every write order follows from this.
- 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.
Why: a stale intent is harmless noise that recovery resolves. An installer-added allow with no journal record is an orphaned permission that uninstall can never distinguish from a user rule, so it leaks forever. The releasing intent exists so that a crash after the settings shrink cannot be mistaken for an owned rule that went missing.
Journal record per rule
One record per target settings file and exact rule string, with a state and a snapshot taken at intent time.
- pending: install intent recorded, settings write not confirmed.
- owned: the installer inserted this allow and may remove it.
- preexisting: the exact allow was already present before install. Never remove it, never re-add it.
- vetoed: a matching deny existed at install time, so the allow was not inserted.
- releasing: uninstall intent recorded with the removal plan, settings shrink not confirmed.
- released: the allow was removed by uninstall, or was observed absent or modified and recorded as a tombstone with a conflict note.
Also store: installer version, the set of managed rule strings for that version, the random witness marker for the current attempt, the pre-write digest of the settings file, and the count of instances inserted.
Journal durability
Write the journal with a temporary file, a flush to disk and an atomic rename, never an in-place overwrite. If the journal exists but cannot be parsed, fail closed: refuse to repair or uninstall the settings, report the damage, and require an explicit force flag whose help text says it may leave installer rules behind or remove rules the user wrote. Never fall back to guessing ownership from rule text.
Commit witness
Preferred: generate a random marker per attempt, record it in the intent, and write it into an installer-namespaced metadata entry in the same atomic settings replacement that adds or removes the rules. For install, marker present after the write means it landed. For uninstall, remove the witness entry in the same write that removes the rules; witness absent and rules absent means the shrink landed.
Fallback when the settings format forbids extra entries: record the pre-write file digest in the intent. Rule present and digest changed means landed. Rule absent and digest unchanged means not landed. Rule present and digest unchanged is impossible under atomic replace. Rule present with a different digest and no witness is ambiguous; report it and keep the record pending rather than guess. This fallback decays once any later writer replaces the file.
Recovery, run first by every command
Install side:
- pending, witness present: promote to owned.
- pending, witness absent, rule absent: the write never landed; discard the intent.
- pending, witness absent, rule present: the write never landed and the developer added the rule afterward; mark preexisting. Do not claim it.
- owned, rule absent: the developer removed or edited it by hand. Record a released tombstone at first observation; see Repair.
Uninstall side, reconciled per record and per target file, never with a global done flag:
- releasing, rule absent, witness absent: the shrink landed. Mark released.
- releasing, rule present, witness present and matching the recorded marker: the shrink never landed. Uninstall rebuilds the plan from the current file, not the stale one, and retries. Repair must not re-add or promote; it reports an interrupted uninstall and asks the user to finish it or reinstall explicitly.
- releasing, rule absent, witness present: the developer deleted the rule inside the window. Mark released; drop the witness on the next write.
- releasing, rule present, witness absent or not matching: the witness was edited or removed by hand. Retry the exact-match removal; the superset invariant makes that safe. Leave a witness that does not match the recorded marker and report it.
Because the settings replacement is atomic, rule and witness cannot be split by a torn write, only by user edits, so every split state is a user action. When several target files are involved, a crash between files leaves some records released and others releasing; the table above resolves each independently and the journal is deleted only when no owned or releasing record remains anywhere.
Deny handling
- A deny that matches a managed rule at install time vetoes insertion. Record vetoed and tell the user.
- 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
Run recovery first. Then for each record:
- owned and present: nothing to do.
- owned and absent: recovery has already tombstoned it as released. Treat the developer's deletion or edit as deliberate. Re-grant only on explicit confirmation in an interactive run or on an explicit reinstall command; a non-interactive repair lists the tombstoned rules and leaves them out. Reason: silently restoring a permission the developer removed is a security surprise, and forgetting the absence makes repair nag forever.
- preexisting and absent: do nothing.
- vetoed with the deny now gone: insert with a fresh intent and promote to owned.
- rules newly added in this installer version: fresh intent, then insert.
- rules the installer version no longer manages but still owns: remove as in uninstall.
Uninstall: compare before remove
- Lock, run recovery, parse the current file and record its digest.
- Build a plan from the current parse. For each owned record: current count of exact matches c, recorded insert count r, remove the smaller of c and r. If that is zero the rule was modified or removed by the developer; plan no change and classify as a conflict. Plan removal of the witness entry only if it still matches the recorded marker.
- Write the releasing intent with the plan and the digest in one atomic journal write.
- Apply the plan to the parsed structure. Touch nothing else: not edited owned rules, not unrelated settings, not denies.
- Re-read and abort if the digest changed since step one. Write settings atomically.
- Mark records released. Delete the journal only when every 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 and would delete it. An in-place edit of an owned rule is indistinguishable from delete plus user add, and exact matching classifies it correctly without extra logic.
Conflict report
Uninstall and repair print one line per record with a fixed category so scripts can parse it:
- removed: exact match found and removed at the planned count.
- left, modified or removed by developer: exact string absent; any similar rule is preserved.
- left, preexisting: present before install.
- left, extra duplicates: more instances than recorded; the surplus stays.
- left, deny present: allow removed, deny untouched.
- left, witness changed: does not match the recorded marker.
- left, found in a different scope file: not owned there.
Offer a dry-run flag that prints the plan and this report without writing. Warn once that re-serialization may normalize formatting or drop comments if the format supports them, and use a format-preserving editor when one exists.
Concurrency and scope
Hold a lock around each read-modify-write pair. Re-read settings immediately before writing and abort if the digest changed since the snapshot. Key journal records by target file so user and project scopes cannot cross-contaminate. Keep the journal in installer state, not inside the settings file.
Anti-patterns
- Inferring ownership from rule text alone.
- Restoring a pre-install snapshot on uninstall.
- Removing a rule that merely resembles an owned rule.
- Writing settings before recording intent.
- Shrinking settings on uninstall without a releasing intent, so repair can undo an interrupted uninstall.
- Tracking uninstall completion with one global flag across several files.
- Deleting the journal before the settings change is confirmed.
- Guessing ownership when the journal is unparseable.
- Silently re-adding an owned rule the developer removed by hand.
- Removing every instance of a managed string on uninstall.
- 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 multiple target files. Journal-first crash on install leaves only a stale intent; settings-first crash leaves an allow with no owner, which is indistinguishable from a user rule and therefore permanent. Uninstall has the mirror asymmetry, and without a releasing intent a repair run after a crashed uninstall re-adds the rules. Retrying an exact-match removal is always safe under the superset invariant, so every split state after a partial uninstall resolves by release or retry. A random marker in the same atomic write is the only witness that user edits cannot forge; a file digest is a weaker fallback. Community conversations independently proposed the commit witness, the smaller-of-counts duplicate rule and the observation-time tombstone; they agree with this analysis but are untrusted and unverified. No tests were executed.