From 39c89bde8623de8e4f8f11e2115e6ee423f7b36d Mon Sep 17 00:00:00 2001 From: dkwolf1 <102389032+dkwolf1@users.noreply.github.com> Date: Tue, 3 Nov 2026 07:43:15 +0100 Subject: [PATCH] Handle active Codex provisioning guards in v3.4.11 --- CHANGELOG.md | 10 ++ README.md | 14 +-- build.ps1 | 2 +- docs/KNOWN-LIMITATIONS.md | 6 +- docs/RELEASE-CHECKLIST.md | 14 +-- docs/nl/KNOWN-LIMITATIONS.md | 6 +- docs/nl/README.md | 13 ++- docs/nl/RELEASE-CHECKLIST.md | 14 +-- docs/nl/releases/v3.4.11.md | 41 ++++++++ docs/releases/v3.4.11.md | 40 +++++++ pyproject.toml | 2 +- src/codex_transfer/__init__.py | 2 +- src/codex_transfer/backup.py | 19 +++- src/codex_transfer/compatibility_matrix.py | 3 +- src/codex_transfer/gui.py | 33 +++++- src/codex_transfer/restore.py | 6 +- src/codex_transfer/selftest.py | 117 ++++++++++++++++++++- version_info.txt | 8 +- 18 files changed, 298 insertions(+), 52 deletions(-) create mode 100644 docs/nl/releases/v3.4.11.md create mode 100644 docs/releases/v3.4.11.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c6e5c9..4a09399 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ ## Unreleased +## 3.4.11 - 2026-11-03 + +- Exclude only root-level `.codex-provisioning-<16-hex>.guard` files from backup, + inventory, safety copies, and legacy restore payloads. The official Codex Windows + sandbox creates these empty delete-on-close guards solely to pin directories + during privileged provisioning; chats, projects, settings, near-matching names, + nested guard files, and all other `*.guard` files remain mandatory. +- Keep the operation header and current status visibly failed after an exception + instead of returning to the misleading `Ready` state. + ## 3.4.10 - 2026-11-02 - Replaced the single `*.dist-info` exception with a complete, read-only preflight diff --git a/README.md b/README.md index 477a499..1b21879 100644 --- a/README.md +++ b/README.md @@ -3,17 +3,17 @@ **One-click Codex backup, restore, and Windows PC migration.** [![Windows](https://img.shields.io/badge/Windows-10%20%7C%2011-0078D4?logo=windows)](https://github.com/dkwolf1/Codex-Lifeboat) -[![Stable: 3.4.10](https://img.shields.io/badge/stable-v3.4.10-16a34a)](https://github.com/dkwolf1/Codex-Lifeboat/releases/tag/v3.4.10) +[![Stable: 3.4.11](https://img.shields.io/badge/stable-v3.4.11-16a34a)](https://github.com/dkwolf1/Codex-Lifeboat/releases/tag/v3.4.11) [![CI](https://github.com/dkwolf1/Codex-Lifeboat/actions/workflows/ci.yml/badge.svg)](https://github.com/dkwolf1/Codex-Lifeboat/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) [Wiki](https://github.com/dkwolf1/Codex-Lifeboat/wiki) · [Nederlands](docs/nl/README.md) · [Security](SECURITY.md) · [Documentation](docs/IMPLEMENTATION-ROADMAP.md) -> **Stable release:** version 3.4.10 preflights complete Codex project dependency -> trees and requires explicit review of the exact inaccessible paths before they -> can be excluded. Large restores use conservative peak-space and runtime reserve -> checks, and rollback is still attempted if a full disk prevents an error-journal -> update. It passes all 95 source checks and 12 automated compatibility scenarios. +> **Stable release:** version 3.4.11 safely ignores only Codex's root-level, +> delete-on-close Windows provisioning guards while keeping every ordinary or +> near-matching file mandatory. It retains the reviewed dependency preflight and +> large-restore protections from 3.4.10, and passes all 97 source checks and 12 +> automated compatibility scenarios. > The migration workflow also has prior > physical Windows 11 PC A → PC B → PC A evidence. Keep an independent copy of > irreplaceable data and read the [testing guide](docs/TESTING-GUIDE.md). @@ -22,7 +22,7 @@ https://mechscript.nl/en/codex-lifeboat-backup-restore-migrate-en/ ## Download for Windows -### [Download Codex Lifeboat 3.4.10 for Windows](https://github.com/dkwolf1/Codex-Lifeboat/releases/tag/v3.4.10) +### [Download Codex Lifeboat 3.4.11 for Windows](https://github.com/dkwolf1/Codex-Lifeboat/releases/tag/v3.4.11) On the release page, download only: diff --git a/build.ps1 b/build.ps1 index eec953b..c50b1c7 100644 --- a/build.ps1 +++ b/build.ps1 @@ -1,6 +1,6 @@ [CmdletBinding()] param( - [string]$Version = '3.4.10', + [string]$Version = '3.4.11', [string]$PythonPath, [switch]$SkipTests ) diff --git a/docs/KNOWN-LIMITATIONS.md b/docs/KNOWN-LIMITATIONS.md index b6939a1..489ad22 100644 --- a/docs/KNOWN-LIMITATIONS.md +++ b/docs/KNOWN-LIMITATIONS.md @@ -1,4 +1,4 @@ -# Known limitations of release 3.4.10 +# Known limitations of release 3.4.11 1. There is no published stable import contract for the local Codex database. The assistant therefore inspects schemas and must be retested after major future @@ -40,11 +40,11 @@ normalization. Edited records, changed metadata, malformed JSONL, an empty rollout, destination-ahead history, and arbitrary divergence require an explicit user decision. Lifeboat does not perform a general-purpose chat merge. -13. Version 3.4.10 is stable after automated source/EXE/ZIP testing and a successful +13. Version 3.4.11 is stable after automated source/EXE/ZIP testing and a successful physical Windows 11 A-to-B-to-A hand-off. A physical Windows 10 run and removal of a real USB drive during an active write are not yet documented. 14. Restore is transactional with automatic rollback but is not yet a general resumable file-copy engine. Very large restores can take substantial time. - Version 3.4.10 conservatively accounts for logical bytes, estimated allocation + Version 3.4.11 conservatively accounts for logical bytes, estimated allocation overhead and a safety reserve, and rechecks free space during large writes; another process can still consume space unexpectedly. diff --git a/docs/RELEASE-CHECKLIST.md b/docs/RELEASE-CHECKLIST.md index dee62ff..e8d7db6 100644 --- a/docs/RELEASE-CHECKLIST.md +++ b/docs/RELEASE-CHECKLIST.md @@ -2,18 +2,18 @@ [Nederlands](nl/RELEASE-CHECKLIST.md) -This checklist publishes Codex Lifeboat 3.4.10 as a **stable release**. The tag +This checklist publishes Codex Lifeboat 3.4.11 as a **stable release**. The tag workflow builds and validates fresh release assets, creates their checksums and provenance attestations, and publishes the GitHub release. ## Before pushing -- Confirm that `git status --short` contains only intended 3.4.10 changes. +- Confirm that `git status --short` contains only intended 3.4.11 changes. - Confirm that no real backup, database, token, `.env`, EXE, ZIP, or build directory is staged. - Keep the generated `dist` directory locally; `.gitignore` deliberately prevents it from entering the repository. -- Do not create the `v3.4.10` tag until the `main` CI run is green. +- Do not create the `v3.4.11` tag until the `main` CI run is green. ## Publish with PowerShell @@ -31,15 +31,15 @@ Wait until the `main` CI run is green at . Then run: ```powershell -git tag -a v3.4.10 -m "Codex Lifeboat 3.4.10 stable release" -git push origin v3.4.10 +git tag -a v3.4.11 -m "Codex Lifeboat 3.4.11 stable release" +git push origin v3.4.11 ``` The tag triggers `.github/workflows/release.yml`. Do **not** also create a manual release with the same tag: the workflow creates it as the stable latest release. Follow it at , then -open . +open . ## Verify the published release @@ -64,7 +64,7 @@ gh attestation verify .\Codex-Lifeboat-Windows-x64-Portable.zip --repo dkwolf1/C ## Stable-release evidence -The 95-check source/EXE/ZIP gate, 12/12 automated Windows scenarios, and a physical +The 97-check source/EXE/ZIP gate, 12/12 automated Windows scenarios, and a physical Windows 11 PC A → PC B → PC A workflow all passed. The physical workflow included both verify operations and opening and continuing work in Codex after restore. Windows 10 and USB-interruption coverage remain documented as pending. Public diff --git a/docs/nl/KNOWN-LIMITATIONS.md b/docs/nl/KNOWN-LIMITATIONS.md index 8b87a8e..40a69f9 100644 --- a/docs/nl/KNOWN-LIMITATIONS.md +++ b/docs/nl/KNOWN-LIMITATIONS.md @@ -1,4 +1,4 @@ -# Bekende grenzen van release 3.4.10 +# Bekende grenzen van release 3.4.11 1. OpenAI publiceert geen stabiel importcontract voor de lokale Codex-database. De app gebruikt daarom schema-inspectie en moet na toekomstige grote @@ -43,11 +43,11 @@ Gewijzigde records of metadata, ongeldige JSONL, een lege rollout, een langere doelchat en iedere andere afwijking vragen een expliciete gebruikerskeuze. Lifeboat voert geen algemene chatmerge uit. -13. Versie 3.4.10 is stabiel na bron-, EXE- en uitgepakte-ZIP-tests en een geslaagde +13. Versie 3.4.11 is stabiel na bron-, EXE- en uitgepakte-ZIP-tests en een geslaagde fysieke Windows 11-route van A naar B en terug. Een fysieke Windows 10-test en het lostrekken van een echte USB-stick tijdens schrijven zijn nog niet vastgelegd. 14. Herstel is transactioneel met automatische rollback, maar nog geen algemeen hervatbare bestandskopieerengine. Zeer grote restores kunnen veel tijd kosten. - Versie 3.4.10 telt logische bytes, geschatte bestandssysteemallocatie en een + Versie 3.4.11 telt logische bytes, geschatte bestandssysteemallocatie en een veiligheidsreserve conservatief mee en controleert vrije ruimte opnieuw tijdens grote schrijfacties; een ander proces kan ruimte nog steeds onverwacht vullen. diff --git a/docs/nl/README.md b/docs/nl/README.md index d6e683b..307f6ea 100644 --- a/docs/nl/README.md +++ b/docs/nl/README.md @@ -4,19 +4,18 @@ [Wiki](https://github.com/dkwolf1/Codex-Lifeboat/wiki) · [English](../../README.md) · [Beveiliging](../../SECURITY.md) · [Documentatie](IMPLEMENTATION-ROADMAP.md) -> **Stabiele release:** versie 3.4.10 controleert vooraf volledige Codex- -> dependencybomen en vereist expliciete goedkeuring van de exacte ontoegankelijke -> paden voordat ze mogen worden uitgesloten. Grote restores gebruiken een -> conservatieve piekruimte- en runtime-reservecontrole; rollback wordt ook gestart -> als een volle schijf het foutjournaal niet kan bijwerken. Alle 95 broncontroles en -> 12 geautomatiseerde compatibiliteitsscenario's slagen. Voor de +> **Stabiele release:** versie 3.4.11 negeert veilig alleen de root-level, +> delete-on-close Windows-provisioningguards van Codex; gewone en bijna gelijknamige +> bestanden blijven verplicht. De gecontroleerde dependency-preflight en +> restorebeveiliging uit 3.4.10 blijven behouden. Alle 97 broncontroles en 12 +> geautomatiseerde compatibiliteitsscenario's slagen. Voor de > migratiewerkwijze bestaat bovendien eerder fysiek bewijs van een Windows 11- > route van pc A naar pc B en terug. Bewaar onvervangbare gegevens ook apart en > lees de [testhandleiding](TESTHANDLEIDING.md). ## Downloaden voor Windows -### [Download Codex Lifeboat 3.4.10](https://github.com/dkwolf1/Codex-Lifeboat/releases/tag/v3.4.10) +### [Download Codex Lifeboat 3.4.11](https://github.com/dkwolf1/Codex-Lifeboat/releases/tag/v3.4.11) Download op de releasepagina uitsluitend dit bestand: diff --git a/docs/nl/RELEASE-CHECKLIST.md b/docs/nl/RELEASE-CHECKLIST.md index 5e30e82..10630ae 100644 --- a/docs/nl/RELEASE-CHECKLIST.md +++ b/docs/nl/RELEASE-CHECKLIST.md @@ -2,16 +2,16 @@ [English](../RELEASE-CHECKLIST.md) -Deze checklist publiceert Codex Lifeboat 3.4.10 als **stabiele release**. De +Deze checklist publiceert Codex Lifeboat 3.4.11 als **stabiele release**. De tag-workflow bouwt en controleert nieuwe releasebestanden, maakt de checksums en herkomstverklaringen en publiceert de GitHub-release. ## Vóór het pushen -- Controleer dat `git status --short` alleen bedoelde 3.4.10-wijzigingen bevat. +- Controleer dat `git status --short` alleen bedoelde 3.4.11-wijzigingen bevat. - Stage geen echte back-up, database, token, `.env`, EXE, ZIP of bouwmap. - De lokale map `dist` hoort niet in Git en wordt bewust genegeerd. -- Maak tag `v3.4.10` pas nadat de CI-run van `main` groen is. +- Maak tag `v3.4.11` pas nadat de CI-run van `main` groen is. ## Publiceren met PowerShell @@ -29,15 +29,15 @@ Wacht tot de `main`-CI groen is op . Voer dan uit: ```powershell -git tag -a v3.4.10 -m "Codex Lifeboat 3.4.10 stable release" -git push origin v3.4.10 +git tag -a v3.4.11 -m "Codex Lifeboat 3.4.11 stable release" +git push origin v3.4.11 ``` De tag start `.github/workflows/release.yml`. Maak niet daarnaast handmatig een release met dezelfde tag: de workflow maakt deze als stabiele nieuwste release. Volg de uitvoering op en open -daarna . +daarna . ## Gepubliceerde release controleren @@ -60,7 +60,7 @@ Controleer na downloaden eventueel de GitHub-herkomstverklaring: gh attestation verify .\Codex-Lifeboat-Windows-x64-Portable.zip --repo dkwolf1/Codex-Lifeboat ``` -De 95 bron-/EXE-/ZIP-controles, 12/12 geautomatiseerde Windows-scenario's en een +De 97 bron-/EXE-/ZIP-controles, 12/12 geautomatiseerde Windows-scenario's en een fysieke Windows 11-route van pc A naar pc B en terug zijn geslaagd. Die route bevatte beide controles en verder werken in Codex na herstel. Windows 10 en USB-onderbreking blijven als open aanvullende dekking vermeld. Testers mogen nooit echte back-ups diff --git a/docs/nl/releases/v3.4.11.md b/docs/nl/releases/v3.4.11.md new file mode 100644 index 0000000..8d922a3 --- /dev/null +++ b/docs/nl/releases/v3.4.11.md @@ -0,0 +1,41 @@ +# Codex Lifeboat 3.4.11 — Windows-provisioningguards + +[English](../../releases/v3.4.11.md) + +Codex Lifeboat 3.4.11 verhelpt een back-upfout door een actieve Windows- +sandboxprovisioningguard direct onder `.codex`. + +## Opgelost + +- Herkent uitsluitend root-level namen volgens + `.codex-provisioning-<16 hexadecimale tekens>.guard`. +- Sluit die guards consequent uit van back-upinventarisatie, het kopiëren van het + draagbare profiel, herstelveiligheidskopieën en legacy-profielpayloads. +- Houdt bijna gelijknamige bestanden, geneste guardbestanden, gewone `*.guard`- + bestanden, gesprekken, projecten, instellingen en registraties verplicht. +- Behoudt een actieve provisioningguard op de doelcomputer wanneer een ouder + package er toevallig één bevat. +- Houdt na een fout de bewerkingskop en huidige status zichtbaar op mislukt, in + plaats van terug te vallen naar het misleidende `Gereed`. + +De officiële Windows-sandbox van Codex maakt deze lege bestanden met +`FILE_DELETE_ON_CLOSE` om een vastgezette map tijdens geprivilegieerde provisioning +niet-leeg te houden. Het is synchronisatie-/runtimestatus en geen gebruikersdata. + +## Controle + +- Positieve dekking toegevoegd voor geldige guardnamen in kleine en hoofdletters. +- Negatieve dekking toegevoegd voor suffixen, ongeldige ID's, geneste guards en + gewone bestanden. +- Back-upinventarisatie, kopiëren, veiligheidskopie en legacy-herstel zijn getest. +- Een regressiecontrole voor de eindstatus van de GUI is toegevoegd. +- Alle 97 broncontroles en alle 12 geautomatiseerde Windows- + compatibiliteitsscenario's slagen. + +## Download + +Download `Codex-Lifeboat-Windows-x64-Portable.zip`, pak het bestand volledig uit en +start `Codex-Lifeboat.exe`. + +De EXE blijft onondertekend. Houd de oude computer en een onafhankelijke +herstelkopie ongewijzigd totdat de doelcomputer alle controles na herstel doorstaat. diff --git a/docs/releases/v3.4.11.md b/docs/releases/v3.4.11.md new file mode 100644 index 0000000..7820a7c --- /dev/null +++ b/docs/releases/v3.4.11.md @@ -0,0 +1,40 @@ +# Codex Lifeboat 3.4.11 — Windows provisioning guard handling + +[Nederlands](../nl/releases/v3.4.11.md) + +Codex Lifeboat 3.4.11 fixes a backup failure caused by a live Windows sandbox +provisioning guard directly below `.codex`. + +## Fixed + +- Recognizes only root-level names matching + `.codex-provisioning-<16 hexadecimal characters>.guard`. +- Excludes those guards consistently from backup inventory, portable-profile + copying, restore safety copies, and legacy portable-profile restore payloads. +- Keeps all near-matching names, nested guard files, ordinary `*.guard` files, + conversations, projects, settings, and registrations mandatory. +- Preserves a destination computer's live provisioning guard when validating an + older package that happens to contain one. +- Keeps the operation header and current status visibly failed after an exception + instead of returning to the misleading `Ready` state. + +The official Codex Windows sandbox creates these empty files with +`FILE_DELETE_ON_CLOSE` to keep a pinned directory nonempty during privileged +provisioning. They are synchronization/runtime state, not user data. + +## Validation + +- Added positive coverage for lowercase and uppercase valid guard names. +- Added negative coverage for suffixes, malformed IDs, nested guard files, and + ordinary files. +- Added backup inventory, copy, safety-copy, and legacy-restore coverage. +- Added a terminal GUI-state regression check. +- All 97 source checks and all 12 automated Windows compatibility scenarios pass. + +## Download + +Download `Codex-Lifeboat-Windows-x64-Portable.zip`, extract it completely, and run +`Codex-Lifeboat.exe`. + +The executable remains unsigned. Keep the old computer and an independent recovery +copy unchanged until the destination has passed post-restore checks. diff --git a/pyproject.toml b/pyproject.toml index 572e91a..1de65cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "codex-lifeboat" -version = "3.4.10" +version = "3.4.11" description = "One-click Codex backup, restore and Windows PC migration utility" readme = "README.md" license = "MIT" diff --git a/src/codex_transfer/__init__.py b/src/codex_transfer/__init__.py index 3a426aa..21ff74c 100644 --- a/src/codex_transfer/__init__.py +++ b/src/codex_transfer/__init__.py @@ -1 +1 @@ -__version__ = "3.4.10" +__version__ = "3.4.11" diff --git a/src/codex_transfer/backup.py b/src/codex_transfer/backup.py index 0e376e5..be019ba 100644 --- a/src/codex_transfer/backup.py +++ b/src/codex_transfer/backup.py @@ -30,7 +30,7 @@ FORMAT_ID = "codex-portable-backup" FORMAT_VERSION = "2.4" -GENERATOR_VERSION = "3.4.10" +GENERATOR_VERSION = "3.4.11" PROGRESS_CALLBACK = None STATUS_CALLBACK = None _RUNTIME_EXECUTABLE: Path | None = None @@ -75,6 +75,9 @@ "process_manager", "thread-writer-locks", } +CODEX_PROVISIONING_GUARD_PATTERN = re.compile( + r"^\.codex-provisioning-[0-9a-f]{16}\.guard$", re.IGNORECASE +) ATTACHMENT_PATTERN = re.compile( r"(?i)([a-z]:[\\/][^\x00\r\n\"<>|?*]{1,2048}?\.(?:png|jpe?g|webp|gif|bmp|pdf|docx?|xlsx?|pptx?|csv|zip))" ) @@ -88,6 +91,11 @@ class BackupError(RuntimeError): pass +def is_codex_provisioning_guard_name(name: str) -> bool: + """Match only Codex's root-level, delete-on-close Windows sandbox guard.""" + return CODEX_PROVISIONING_GUARD_PATTERN.fullmatch(str(name)) is not None + + def new_path_budget() -> dict[str, Any]: return { "longest": None, @@ -1118,7 +1126,12 @@ def _portable_profile_stats( lowered = entry.name.lower() if lowered in {value.lower() for value in handled_names}: continue - if lowered in blocked_names or lowered in blocked_dirs or entry.is_symlink(): + if ( + lowered in blocked_names + or lowered in blocked_dirs + or is_codex_provisioning_guard_name(entry.name) + or entry.is_symlink() + ): continue package_prefix = f"codex/portable-profile/{entry.name}" if entry.is_dir(): @@ -1944,6 +1957,8 @@ def copy_portable_codex_profile( continue if lowered in {value.lower() for value in BLOCKED_CODEX_DIRS}: continue + if is_codex_provisioning_guard_name(entry.name): + continue relative = f"codex/portable-profile/{entry.name}" if entry.is_symlink(): warnings.append(f"Codex symbolic link or reparse point skipped: {entry}") diff --git a/src/codex_transfer/compatibility_matrix.py b/src/codex_transfer/compatibility_matrix.py index ac6fc1a..44ac243 100644 --- a/src/codex_transfer/compatibility_matrix.py +++ b/src/codex_transfer/compatibility_matrix.py @@ -65,6 +65,7 @@ "dependencyExclusionsRequireExplicitApproval", "dependencyExclusionReportRoundTrips", "otherUnreadableDirectoriesFailClosed", + "provisioningGuardRuntimeExcluded", ), ), ( @@ -80,7 +81,7 @@ ( "progress-state-transitions", "Measured and non-measurable work have unambiguous progress states", - ("progressTransitionsClear",), + ("progressTransitionsClear", "operationFailureStatusPersists"), ), ) diff --git a/src/codex_transfer/gui.py b/src/codex_transfer/gui.py index 26160e9..a484dc9 100644 --- a/src/codex_transfer/gui.py +++ b/src/codex_transfer/gui.py @@ -28,6 +28,12 @@ def weighted_operation_progress( return min(100.0, max(float(previous), candidate)) +def operation_status_key(busy: bool, failed: bool) -> str: + if busy: + return "working" + return "failed" if failed else "ready" + + def backup_destination_initial_directory( drives: list[Path], fallback: Path ) -> str: @@ -233,9 +239,12 @@ def ask_backup_destination(parent, title: str, initial_directory: str) -> str: "diagnostics_portability_audit": "Overdraagbaarheid van paden", "diagnostics_atomic_metadata": "Atomair opslaan van metadata", "ready": "Gereed", + "failed": "Mislukt", "working": "Bezig… sluit dit venster niet.", "operation_idle_title": "Klaar voor een veilige overdracht", "operation_idle_subtitle": "Maak een back-up of zet er één terug wanneer u wilt.", + "operation_failed_title": "Bewerking mislukt", + "operation_failed_subtitle": "Er is niets voltooid. Bekijk de technische details voordat u het opnieuw probeert.", "operation_backup_title": "Back-up wordt gemaakt", "operation_verify_title": "Back-up wordt gecontroleerd", "operation_restore_title": "Back-up wordt teruggezet", @@ -515,9 +524,12 @@ def ask_backup_destination(parent, title: str, initial_directory: str) -> str: "diagnostics_portability_audit": "Path portability", "diagnostics_atomic_metadata": "Atomic metadata storage", "ready": "Ready", + "failed": "Failed", "working": "Working… do not close this window.", "operation_idle_title": "Ready for a safe transfer", "operation_idle_subtitle": "Create or restore a backup whenever you are ready.", + "operation_failed_title": "Operation failed", + "operation_failed_subtitle": "Nothing was completed. Review the technical details before trying again.", "operation_backup_title": "Backup in progress", "operation_verify_title": "Verifying backup", "operation_restore_title": "Restoring backup", @@ -2312,6 +2324,7 @@ def __init__(self) -> None: self.last_result_verified = False self.current_tab = "overview" self.operation_key = "idle" + self.operation_failed = False self.operation_started_at: float | None = None self.phase_started_at: float | None = None self.phase_signature = "" @@ -3013,6 +3026,7 @@ def _operation_title_key(self) -> str: def _begin_operation(self, operation: str | None = None) -> None: self.operation_key = operation or "other" + self.operation_failed = False self.operation_started_at = time.monotonic() self.phase_started_at = self.operation_started_at self.phase_signature = "" @@ -3028,6 +3042,7 @@ def _begin_operation(self, operation: str | None = None) -> None: self._render_operation_state() def _finish_operation(self, success: bool) -> None: + self.operation_failed = not success if self.operation_started_at is not None: self.elapsed_label.configure( text=f"{self.t('elapsed')}: " @@ -3145,6 +3160,9 @@ def _render_operation_state(self) -> None: if self.busy: self.operation_title.configure(text=self.t(self._operation_title_key())) self.operation_subtitle.configure(text=self.t("source_unchanged")) + elif self.operation_failed: + self.operation_title.configure(text=self.t("operation_failed_title")) + self.operation_subtitle.configure(text=self.t("operation_failed_subtitle")) else: self.operation_title.configure(text=self.t("operation_idle_title")) self.operation_subtitle.configure(text=self.t("operation_idle_subtitle")) @@ -3227,18 +3245,23 @@ def _translate(self) -> None: text="● " + self.t("warning"), bg="#FFF4E5", fg="#A15C00" ) else: + terminal_key = operation_status_key(self.busy, self.operation_failed) self.status_label.configure( - text=f"{self.t('current_task')}: {self.last_progress_message or self.t('working')}" - if self.busy else self.t("ready") + text=( + f"{self.t('current_task')}: " + f"{self.last_progress_message or self.t('working')}" + if self.busy + else self.t(terminal_key) + ) ) if not self.busy and self.operation_started_at is None: self.elapsed_label.configure(text=f"{self.t('elapsed')}: —") self.remaining_label.configure(text=f"{self.t('remaining')}: —") self.throughput_label.configure(text=f"{self.t('throughput')}: —") self.header_status.configure( - text="● " + (self.t("working") if self.busy else self.t("ready")), - bg="#EAF2FD" if self.busy else "#E8F7EF", - fg=self.BLUE if self.busy else "#128250", + text="● " + self.t(terminal_key), + bg=("#EAF2FD" if self.busy else "#FDECEC" if self.operation_failed else "#E8F7EF"), + fg=(self.BLUE if self.busy else "#B42318" if self.operation_failed else "#128250"), ) def _render_last_result(self) -> None: diff --git a/src/codex_transfer/restore.py b/src/codex_transfer/restore.py index a2f0b24..60397e5 100644 --- a/src/codex_transfer/restore.py +++ b/src/codex_transfer/restore.py @@ -28,7 +28,7 @@ from validate import validate -RESTORE_VERSION = "3.4.10" +RESTORE_VERSION = "3.4.11" PROTECTED_NAMES = {"auth.json", "installation_id", "cap_sid"} RUNTIME_NAMES = { ".sandbox", @@ -270,6 +270,8 @@ def _copy_codex_safety(source_codex: Path, safety_codex: Path) -> None: continue if lowered in {value.lower() for value in RUNTIME_NAMES}: continue + if backup.is_codex_provisioning_guard_name(entry.name): + continue destination = safety_codex / entry.name if entry.is_symlink(): continue @@ -1176,6 +1178,8 @@ def _restore_portable_profile( continue if entry.name.lower() in {value.lower() for value in PROTECTED_NAMES | RUNTIME_NAMES}: continue + if backup.is_codex_provisioning_guard_name(entry.name): + continue if destination.exists(): _remove_path(destination) if entry.is_dir(): diff --git a/src/codex_transfer/selftest.py b/src/codex_transfer/selftest.py index 811b39a..0d38a79 100644 --- a/src/codex_transfer/selftest.py +++ b/src/codex_transfer/selftest.py @@ -763,7 +763,7 @@ def _write_older_target(profile: Path, marker: str) -> tuple[Path, Path]: def _progress_model_test() -> bool: - from .gui import TransferApp, weighted_operation_progress + from .gui import TransferApp, operation_status_key, weighted_operation_progress progress = weighted_operation_progress(0, 0, 5, 10) progress = weighted_operation_progress(progress, 1, 0, 0) @@ -781,6 +781,19 @@ def _progress_model_test() -> bool: and TransferApp._progress_stage("Inventorying conversation attachments") == 2 and TransferApp._progress_stage("Hashing backup: 5/10") == 3 and TransferApp._progress_stage("Backup complete") == 4 + and operation_status_key(True, False) == "working" + and operation_status_key(False, False) == "ready" + and operation_status_key(False, True) == "failed" + ) + + +def _operation_failure_status_test() -> bool: + from .gui import operation_status_key + + return bool( + operation_status_key(True, False) == "working" + and operation_status_key(False, False) == "ready" + and operation_status_key(False, True) == "failed" ) @@ -866,6 +879,17 @@ def _gui_smoke_test() -> bool: ) app._finish_operation(True) app._set_busy(False) + app._begin_operation("backup") + app._set_busy(True) + app._finish_operation(False) + app._set_busy(False) + app.update_idletasks() + failure_ui_persists = bool( + app.operation_title.cget("text") == "Operation failed" + and app.status_label.cget("text") == "Failed" + and app.header_status.cget("text") == "● Failed" + and app.progress_percent.cget("foreground") == "#B42318" + ) blocked_dialog = RestorePlanDialog( app, { @@ -1120,7 +1144,8 @@ def _gui_smoke_test() -> bool: ) diagnostics_dialog.destroy() return bool( - dutch and english and progress_ui_stable and navigation_tabs + dutch and english and progress_ui_stable and failure_ui_persists + and navigation_tabs and len(app.action_buttons) == 6 and blocked_is_disabled and ready_is_enabled and decision_button_enabled and decision_resolved @@ -1548,6 +1573,92 @@ def text(value: str) -> str: ) +def _provisioning_guard_policy_test(root: Path) -> bool: + fixture = root / "provisioning-guard-policy" + source_codex = fixture / "source-user" / ".codex" + package_root = fixture / "package" + target_codex = fixture / "target-user" / ".codex" + safety_codex = fixture / "safety" / "codex-before-restore" + source_codex.mkdir(parents=True) + target_codex.mkdir(parents=True) + + exact_names = { + ".codex-provisioning-0123456789abcdef.guard", + ".CODEX-PROVISIONING-FEDCBA9876543210.GUARD", + } + for name in exact_names: + (source_codex / name).write_bytes(b"") + near_match = source_codex / ".codex-provisioning-0123456789abcdef.guard.bak" + near_match.write_bytes(b"ordinary-root-data") + nested_guard = ( + source_codex + / "skills" + / "demo" + / ".codex-provisioning-0123456789abcdef.guard" + ) + nested_guard.parent.mkdir(parents=True) + nested_guard.write_bytes(b"ordinary-nested-data") + (source_codex / "config.toml").write_text("portable = true\n", encoding="utf-8") + + original_copy_file = backup.copy_file + + def reject_root_guard(source: Path, destination: Path) -> None: + if ( + source.parent == source_codex + and backup.is_codex_provisioning_guard_name(source.name) + ): + raise PermissionError(13, "runtime guard must not be copied", str(source)) + original_copy_file(source, destination) + + stats_files, _stats_bytes, stats_details = backup._portable_profile_stats( + source_codex + ) + with mock.patch.object(backup, "copy_file", side_effect=reject_root_guard): + records = backup.copy_portable_codex_profile(source_codex, package_root, []) + + portable_root = package_root / "codex" / "portable-profile" + exact_absent = all(not (portable_root / name).exists() for name in exact_names) + record_names = {str(item.get("name", "")) for item in records} + detail_names = {str(item.get("name", "")) for item in stats_details} + ordinary_preserved = bool( + (portable_root / near_match.name).read_bytes() == b"ordinary-root-data" + and (portable_root / "skills" / "demo" / nested_guard.name).read_bytes() + == b"ordinary-nested-data" + ) + + legacy_name = ".codex-provisioning-0011223344556677.guard" + (portable_root / legacy_name).write_bytes(b"legacy-runtime-guard") + target_guard = target_codex / legacy_name + target_guard.write_bytes(b"live-target-runtime-guard") + + class IdentityTranslator: + @staticmethod + def text(value: str) -> str: + return value + + restored = restore._restore_portable_profile( + package_root, target_codex, IdentityTranslator() + ) + restore_preserved_target = target_guard.read_bytes() == b"live-target-runtime-guard" + restore._copy_codex_safety(source_codex, safety_codex) + safety_skipped = all(not (safety_codex / name).exists() for name in exact_names) + + return bool( + backup.is_codex_provisioning_guard_name(legacy_name) + and not backup.is_codex_provisioning_guard_name(near_match.name) + and stats_files == 3 + and exact_absent + and exact_names.isdisjoint(record_names) + and exact_names.isdisjoint(detail_names) + and {near_match.name, "skills", "config.toml"}.issubset(record_names) + and {near_match.name, "skills", "config.toml"}.issubset(detail_names) + and ordinary_preserved + and restore_preserved_target + and safety_skipped + and restored == 3 + ) + + def _paginated_history_projection_policy_test(root: Path) -> bool: fixture = root / "paginated-history-projection-policy" source_codex = fixture / "source-user" / ".codex" @@ -4649,6 +4760,7 @@ def fail_error_journal(path: Path, value: Any) -> None: "completePhase3Inventory": inventory_complete, "attachmentDetectionAndAccessResilience": _attachment_resilience_test(root), "pluginRuntimeExcludedAndTargetPreserved": _plugin_runtime_policy_test(root), + "provisioningGuardRuntimeExcluded": _provisioning_guard_policy_test(root), "projectRootInventoryAnalysis": _project_inventory_analysis_test(root), "lineageChangeModel": _lineage_model_test(), "streamingLineageDigest": _streaming_lineage_digest_test(root), @@ -4700,6 +4812,7 @@ def fail_error_journal(path: Path, value: Any) -> None: "safetyCopyKept": Path(prepared["safetyRoot"]).is_dir(), "newerSourceToOlderTarget": older_schema_restore_valid, "stableOperationProgress": _progress_model_test(), + "operationFailureStatusPersists": _operation_failure_status_test(), "dutchEnglishGui": _gui_smoke_test(), } matrix = compatibility_matrix.build_matrix(checks) diff --git a/version_info.txt b/version_info.txt index aade882..ab91e78 100644 --- a/version_info.txt +++ b/version_info.txt @@ -1,7 +1,7 @@ VSVersionInfo( ffi=FixedFileInfo( - filevers=(3, 4, 10, 0), - prodvers=(3, 4, 10, 0), + filevers=(3, 4, 11, 0), + prodvers=(3, 4, 11, 0), mask=0x3f, flags=0x0, OS=0x40004, @@ -16,11 +16,11 @@ VSVersionInfo( [ StringStruct(u'CompanyName', u'Codex Lifeboat community project'), StringStruct(u'FileDescription', u'Codex Lifeboat'), - StringStruct(u'FileVersion', u'3.4.10'), + StringStruct(u'FileVersion', u'3.4.11'), StringStruct(u'InternalName', u'Codex-Lifeboat'), StringStruct(u'OriginalFilename', u'Codex-Lifeboat.exe'), StringStruct(u'ProductName', u'Codex Lifeboat'), - StringStruct(u'ProductVersion', u'3.4.10') + StringStruct(u'ProductVersion', u'3.4.11') ] ) ]),