# Phase 19.3: installed-system rollback workflow on FreeBSD Date: 2026-04-04 ## Goal Phase 19.3 is about validating installed-system rollback through the intended operator-facing workflow, not only through host-side build/image redeploy harnesses. The key question was: - can an already-installed Fruix system move between recorded generations coherently, using an operator-facing command surface on the target itself? ## Decision The current Fruix solution is intentionally modest. Fruix now provides a small installed-system helper on the target itself: - `/usr/local/bin/fruix` Validated in-guest commands: - `fruix system status` - `fruix system switch /frx/store/...-fruix-system-...` - `fruix system rollback` Important scope choice: - `switch` assumes the candidate closure is already present on the target's `/frx/store` - Fruix does **not** yet fetch or transfer that closure onto the target automatically That keeps Phase 19.3 focused on generation-state correctness rather than introducing a larger store-transfer story prematurely. ## Implemented model Installed systems now support the following validated operator pattern: 1. build a candidate closure with the host-side Fruix frontend 2. stage that closure into the installed system's `/frx/store` 3. run: - `fruix system switch /frx/store/...candidate...` 4. reboot into the candidate generation 5. if needed, run: - `fruix system rollback` 6. reboot into the recorded rollback generation The installed system now records explicit rollback state under: ```text /var/lib/fruix/system/ current -> generations/N current-generation rollback -> generations/M rollback-generation generations/ ... ``` and explicit rollback reachability under: ```text /frx/var/fruix/gcroots/ current-system -> /frx/store/...current... rollback-system -> /frx/store/...rollback... system-1 -> ... system-2 -> ... ``` ## Code changes ### `modules/fruix/system/freebsd/render.scm` Added a generated in-guest Fruix deployment helper script under: - `usr/local/bin/fruix` That helper now: - reports installed-system state with `fruix system status` - stages a new current generation with `fruix system switch` - stages the recorded rollback generation with `fruix system rollback` - updates: - `/var/lib/fruix/system/current` - `/var/lib/fruix/system/current-generation` - `/var/lib/fruix/system/rollback` - `/var/lib/fruix/system/rollback-generation` - `/frx/var/fruix/gcroots/current-system` - `/frx/var/fruix/gcroots/rollback-system` - `/frx/var/fruix/gcroots/system-N` - refreshes the ESP bootloader file from the selected closure's `boot/loader.efi` A practical implementation detail mattered here: - replacing `/run/current-system` with a remove-then-recreate strategy caused the live shell environment to break while the link was absent - switching that update to an atomic symlink replacement path for `/run/current-system` avoided that gap and made the in-guest operator command reliable ### `modules/fruix/system/freebsd/media.scm` Updated installed rootfs staging so that installed targets expose: - `/usr/local/bin/fruix -> /run/current-system/usr/local/bin/fruix` Also bumped the explicit generation-layout version from: - `1` to `2` because the installed-system model now includes operator-driven switch/rollback state as part of the validated layout story. ### `modules/fruix/system/freebsd/model.scm` Updated generated-file metadata so the system closure records: - `usr/local/bin/fruix` as part of the generated operating-system file set. ## New validation harness Added: - `tests/system/run-phase19-installed-system-rollback-qemu.sh` This harness validates the actual installed-system operator flow on local `QEMU/UEFI/TCG`. ## Validation flow The harness now performs all of the following: 1. installs a current system image directly to a target disk image 2. builds a distinct candidate closure - in the validated harness this differs by host name so the closure identity changes cleanly without needing a heavier base-version rebuild 3. stages the candidate closure and its referenced store items into the installed target's `/frx/store` 4. boots the installed current system 5. validates initial state: - current generation = `1` - current closure = current installed closure - no rollback generation yet recorded 6. runs: - `fruix system switch /frx/store/...candidate...` 7. validates staged switch state: - current generation = `2` - rollback generation = `1` - current closure = candidate closure - rollback closure = original current closure - generation 2 metadata/install files were written 8. reboots and validates boot into the candidate closure 9. runs: - `fruix system rollback` 10. validates staged rollback state: - current generation = `1` - rollback generation = `2` - current closure = original closure - rollback closure = candidate closure 11. reboots and validates boot back into the original current system 12. confirms post-rollback service state: - `fruix-shepherd` running - `sshd` running - activation log still shows success ## Passing validation Passing result: - `PASS phase19-installed-system-rollback-qemu` Validated metadata summary: ```text current_closure_path=/frx/store/4debd106d62f14594ba1612e1e7105f1658bf5f4075d6e5db5436efeaf929d90-fruix-system-fruix-freebsd-current candidate_closure_path=/frx/store/54fb14e6071b8e5704a5dc75e2881c2f0533767771c26c4181f57afea88d1e8b-fruix-system-fruix-freebsd-canary current_host_name=fruix-freebsd-current candidate_host_name=fruix-freebsd-canary final_current_generation=1 final_current_closure=/frx/store/4debd106d62f14594ba1612e1e7105f1658bf5f4075d6e5db5436efeaf929d90-fruix-system-fruix-freebsd-current final_rollback_generation=2 final_rollback_closure=/frx/store/54fb14e6071b8e5704a5dc75e2881c2f0533767771c26c4181f57afea88d1e8b-fruix-system-fruix-freebsd-canary installed_system_switch=ok installed_system_rollback=ok ``` ## Regression checks After landing the installed-system switch/rollback workflow, the following regression checks still pass: - `PASS phase19-generation-layout-qemu` - `PASS phase18-installer-iso` That means the new in-guest generation-management path did not regress: - the previously validated explicit generation layout - or the UEFI installer ISO boot/install path ## Relationship to Guix This phase does **not** claim that Fruix now matches Guix's full installed-system UX. What Fruix now has is: - explicit generation state on disk - explicit current/rollback pointers - a minimal installed-system operator command surface - validated switching and rollback between already-staged closures What still remains compared with Guix: - building/staging the candidate closure from inside the target system itself - automatic closure transfer/fetch as part of `switch` - a richer long-term generation lifecycle policy ## Conclusion Phase 19.3 is complete. Fruix now validates an actual installed-system rollback workflow on FreeBSD: - the target system itself can report current/rollback state - it can switch to a staged candidate generation - it can reboot into that candidate generation - it can roll back to the recorded prior generation - and it can reboot into the restored current system That closes the Phase 19 deployment story from: - documented deployment workflow - to explicit generation layout - to validated installed-system operator rollback behavior