diff --git a/CHANGELOG.md b/CHANGELOG.md index 88d36f2..4e9007d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,24 @@ current uv-based install, development, and release instructions. ## [Unreleased] +## [2.0.5] - 2026-10-22 + +### Fixed + +- Central cloud retention honors an explicitly configured positive deletion + grace period instead of imposing a thirty-day minimum. Thirty days remains + the recommended profile; shorter grace reduces historical protection and + does not establish a thirty-day recovery guarantee. Live aged reclamation + remains unqualified under issue #25. + +### Changed + +- Issues #23–#26 were canceled and closed as not planned. Their implementations + remain experimental and unqualified. This release adds no new live provider + qualification; the existing workstation results remain historical. +- Stable support remains at the v2.0.4 tested core scope; the retention policy + change adds no provider or recovery qualification. + ## [2.0.4] - 2026-10-21 ### Added @@ -653,7 +671,8 @@ are still under development. See [implementation status](docs/development/versio The `Unreleased` comparison is a live `HEAD` link for navigation, not an immutable historical citation. -[Unreleased]: https://github.com/CruxExperts/BestBackup/compare/v2.0.4...HEAD +[Unreleased]: https://github.com/CruxExperts/BestBackup/compare/v2.0.5...HEAD +[2.0.5]: https://github.com/CruxExperts/BestBackup/compare/v2.0.4...v2.0.5 [2.0.4]: https://github.com/CruxExperts/BestBackup/compare/v2.0.3...v2.0.4 [2.0.3]: https://github.com/CruxExperts/BestBackup/compare/v2.0.2...v2.0.3 [2.0.2]: https://github.com/CruxExperts/BestBackup/compare/v2.0.1...v2.0.2 diff --git a/INSTALL.md b/INSTALL.md index a8c88e0..2628352 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -1,6 +1,6 @@ # Install bbackup -This guide covers stable v2.0.4 for its tested file, database, dashboard, and AMD64 package scope. New cloud and Linux recovery workflows are experimental and unqualified; B2/AWS, native Docker, ARM64 package functionality, and broader recovery guarantees remain outside the qualified scope. The retained 1.x tag remains available as v1.8.6. +This guide covers stable v2.0.5 for its tested file, database, dashboard, and AMD64 package scope. New cloud and Linux recovery workflows are experimental and unqualified; B2/AWS, native Docker, ARM64 package functionality, and broader recovery guarantees remain outside the qualified scope. The retained 1.x tag remains available as v1.8.6. ## Version 2 stable release @@ -9,12 +9,12 @@ The stable release uses Python 3.12 or newer and the `restic` executable. Instal Install the pinned stable release with uv: ```bash -uv tool install --force 'git+https://github.com/CruxExperts/BestBackup.git@v2.0.4' +uv tool install --force 'git+https://github.com/CruxExperts/BestBackup.git@v2.0.5' bbackup --version bbackup production --help ``` -The command is pinned to the `v2.0.4` source tag. To inspect or modify the source, use the development checkout below. +The command is pinned to the `v2.0.5` source tag. To inspect or modify the source, use the development checkout below. ### Optional native database clients @@ -22,11 +22,11 @@ PostgreSQL capture requires `pg_dump` and `psql` for source metadata; restoring ### Optional systemd scheduling -Version 2.0.4 supports explicit user-level installation through `service install`, with activation only when `--activate` is supplied. Scheduled file capture/copy/restore, missed-timer catch-up, and abrupt interruption without replay passed isolated Ubuntu 24.04/26.04 AMD64 guest drills; this does not qualify every host configuration or the root-owned Debian templates. Review [the quick start](QUICKSTART.md#apply-a-schedule-deliberately) and the [development candidate reboot qualification](docs/development/version-2.md#development-candidate-rebootable-systemd-qualification) before enabling a schedule. A user timer needs a user systemd manager and may need administrator-managed lingering to run after logout or at boot. +Version 2.0.5 supports explicit user-level installation through `service install`, with activation only when `--activate` is supplied. Scheduled file capture/copy/restore, missed-timer catch-up, and abrupt interruption without replay passed isolated Ubuntu 24.04/26.04 AMD64 guest drills; this does not qualify every host configuration or the root-owned Debian templates. Review [the quick start](QUICKSTART.md#apply-a-schedule-deliberately) and the [development candidate reboot qualification](docs/development/version-2.md#development-candidate-rebootable-systemd-qualification) before enabling a schedule. A user timer needs a user systemd manager and may need administrator-managed lingering to run after logout or at boot. ### Cloud repositories -Restic repositories can use local storage, Backblaze B2 through the S3 API, or Amazon S3. Cloud providers are configurable but not production-qualified in v2.0.4. Configure credentials in the supervised process environment. Do not place credentials in portable JSON policy or a repository URL. See [cloud storage](docs/cloud-storage.md) for experimental provider setup and limits. +Restic repositories can use local storage, Backblaze B2 through the S3 API, or Amazon S3. Cloud providers are configurable but not production-qualified in v2.0.5. Configure credentials in the supervised process environment. Do not place credentials in portable JSON policy or a repository URL. See [cloud storage](docs/cloud-storage.md) for experimental provider setup and limits. ## Existing 1.x release @@ -81,7 +81,7 @@ sudo env \ UV_TOOL_DIR=/opt/uv/tools \ UV_TOOL_BIN_DIR=/usr/local/bin \ "$UV" tool install --force \ - 'git+https://github.com/CruxExperts/BestBackup.git@v2.0.4' + 'git+https://github.com/CruxExperts/BestBackup.git@v2.0.5' bbackup --version ``` @@ -113,12 +113,12 @@ Install a newer release tag with `uv tool install --force` and the exact tag. Pr ## Debian package installation -Native Ubuntu packages target Ubuntu 24.04 with Python 3.12 and Ubuntu 26.04 with Python 3.14, on amd64 or arm64. The v2.0.4 production-qualified package scope is amd64 only; ARM64 artifacts, if built, are unqualified pending functional capture/restore smoke tests. Use a package built natively for the target release, architecture, and Python version; the package builder refuses cross-build labels. +Native Ubuntu packages target Ubuntu 24.04 with Python 3.12 and Ubuntu 26.04 with Python 3.14, on amd64 or arm64. The v2.0.5 production-qualified package scope is amd64 only; ARM64 artifacts, if built, are unqualified pending functional capture/restore smoke tests. Use a package built natively for the target release, architecture, and Python version; the package builder refuses cross-build labels. Inspect the package, contents, manifest, and checksum before installing. For the Ubuntu 24.04 AMD64 artifact: ```bash -PACKAGE=dist/deb/bbackup_2.0.4_ubuntu24.04_amd64.deb +PACKAGE=dist/deb/bbackup_2.0.5_ubuntu24.04_amd64.deb (cd "$(dirname "$PACKAGE")" && sha256sum -c "$(basename "${PACKAGE%.deb}.sha256")") dpkg-deb --info "$PACKAGE" dpkg-deb --contents "$PACKAGE" @@ -130,13 +130,13 @@ cat "$REVIEW_DIR/usr/share/doc/bbackup/build-manifest.json" Install only after reviewing that output: ```bash -sudo apt install ./dist/deb/bbackup_2.0.4_ubuntu24.04_amd64.deb +sudo apt install ./dist/deb/bbackup_2.0.5_ubuntu24.04_amd64.deb bbackup --version bbman --help ``` The release build matrix targets four uniquely named native packages: `bbackup__ubuntu24.04_amd64.deb`, `bbackup__ubuntu24.04_arm64.deb`, `bbackup__ubuntu26.04_amd64.deb`, and `bbackup__ubuntu26.04_arm64.deb`. A configured matrix is not evidence that all four artifacts have been built or qualified. Each package contains the versioned Python package, its locked dependencies, and the verified restic binary. Installation does not install Python packages into system Python, access a package index at runtime, enable or start services, or remove configuration, private bindings, credentials, repositories, or backup state. -The v2.0.4 release workflow also builds ARM64 packages, but its install smoke does not qualify backup/restore functionality on ARM64. Treat those packages as experimental. +The v2.0.5 release workflow also builds ARM64 packages, but its install smoke does not qualify backup/restore functionality on ARM64. Treat those packages as experimental. Package upgrades and removals refuse while either command is active. A root-owned marker blocks both launchers while package files are changing. Successful configuration verifies the installed payload checksum manifest before clearing the marker. On abort, a marker requires the exclusive process lock and complete payload validation before removal. Without a marker, the abort hook takes a shared lock, verifies the payload and launchers, and returns without clearing anything; existing clients can continue while transition writers remain excluded. diff --git a/QUICKSTART.md b/QUICKSTART.md index c2e4a14..c4b25df 100644 --- a/QUICKSTART.md +++ b/QUICKSTART.md @@ -1,6 +1,6 @@ # Your first backup and verified restore -This guide covers v2.0.4 and its schema-3 policy. For an older v2.0.2 installation, use its [versioned quick start](https://github.com/CruxExperts/BestBackup/blob/v2.0.2/QUICKSTART.md) until you migrate. Keep an independently retrievable copy of each repository password. +This guide covers v2.0.5 and its schema-3 policy. For an older v2.0.2 installation, use its [versioned quick start](https://github.com/CruxExperts/BestBackup/blob/v2.0.2/QUICKSTART.md) until you migrate. Keep an independently retrievable copy of each repository password. ## Start with the guided dashboard diff --git a/README.md b/README.md index 2039324..5c84876 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ bbackup is an encrypted, restic-based backup tool for Linux system administrators, homelab and self-hosted operators, and small infrastructure teams. It brings local file backups, selected database workflows, verified file restores, and observable scheduled jobs into one operator workflow. -[![Python 3.12+](https://img.shields.io/badge/python-3.12%2B-3776ab?style=flat-square&logo=python&logoColor=white)](https://www.python.org/downloads/) [![License: MIT](https://img.shields.io/badge/license-MIT-22c55e?style=flat-square)](LICENSE) [![Version 2.0.4](https://img.shields.io/badge/version-2.0.4-22c55e?style=flat-square)](https://github.com/CruxExperts/BestBackup/releases/latest) +[![Python 3.12+](https://img.shields.io/badge/python-3.12%2B-3776ab?style=flat-square&logo=python&logoColor=white)](https://www.python.org/downloads/) [![License: MIT](https://img.shields.io/badge/license-MIT-22c55e?style=flat-square)](LICENSE) [![Version 2.0.5](https://img.shields.io/badge/version-2.0.5-22c55e?style=flat-square)](https://github.com/CruxExperts/BestBackup/releases/latest) ![bbackup: encrypted snapshot layers, an additional copy, and a restored document](docs/assets/bbackup-banner-v2.png) @@ -22,9 +22,9 @@ Use bbackup when you want to know what a job captured, whether each repository c ![Real bbackup dashboard with disposable example data and illustrative capacity](docs/assets/bbackup-dashboard-preview.svg) -## v2.0.4 tested scope +## v2.0.5 tested scope -The qualified scope covers local file capture and restore; live capture and restore for PostgreSQL 17.11, MySQL 8.4.12, and MariaDB 11.8.9; headless dashboard happy paths; and AMD64 Ubuntu 24.04/26.04 packages. A separate real-guest user-timer reboot drill covers only the components and conditions recorded in the [qualification evidence](docs/development/version-2.md#development-candidate-rebootable-systemd-qualification). +The qualified scope is unchanged from v2.0.4: local file capture and restore; live capture and restore for PostgreSQL 17.11, MySQL 8.4.12, and MariaDB 11.8.9; headless dashboard happy paths; and AMD64 Ubuntu 24.04/26.04 packages. A separate real-guest user-timer reboot drill covers only the components and conditions recorded in the [qualification evidence](docs/development/version-2.md#development-candidate-rebootable-systemd-qualification). B2/AWS, native Docker, ARM64 backup/restore, cross-application-version upgrades and rollback, root-owned systemd package templates, and cloud or off-host protection remain unqualified. ARM64 packages may be built, but build or package-install smoke checks alone do not qualify backup/restore. Keep an independently retrievable copy of repository passwords; a second repository on the same host does not protect against host loss. See [installation](INSTALL.md) for prerequisites and release install instructions. @@ -32,15 +32,18 @@ Recent live B2 evidence covers a small interrupted maintenance run whose prune outcome remained uncertain, and a separate lifecycle-overlap guard that made central retention refuse the configured bucket. Neither test proves prune completion, elapsed lifecycle expiry, physical reclamation, or provider -protection. These workflows ship in v2.0.4 as experimental and remain +protection. These workflows ship in v2.0.5 as experimental and remain unqualified. See the [maintenance interruption record](docs/development/version-2.md#live-b2-central-maintenance-interruption-trial) and [lifecycle guard evidence](docs/cloud-storage.md#b2-lifecycle-overlap-guard-experimental). -Requests #23–#26 remain open; the qualified core scope is unchanged from v2.0.3. +Issues #23–#26 were canceled and closed as not planned. Cancellation does not +qualify the experimental B2 workflow. This release adds no new live provider +qualification; the existing workstation results remain historical. +The qualified core scope is unchanged from v2.0.4. ## Start with the guided workflow Experimental restricted cloud credentials, central recovery and retention, -bounded remote-primary capture, and reviewed Linux coverage ship in v2.0.4 and +bounded remote-primary capture, and reviewed Linux coverage ship in v2.0.5 and are [documented with their limits](docs/development/version-2.md#experimental-cloud-and-machine-recovery-features). The reviewed Linux coverage work includes an experimental manual PostgreSQL path. These features do not expand the tested stable scope. diff --git a/SECURITY.md b/SECURITY.md index ddc1bfc..d7bc27f 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,7 +4,7 @@ This policy covers security issues in bbackup's source code and the packaged com ## Version status -Version 2.0.3 includes the reviewed safety and schema-3 enrollment changes. Supported scope and historical release boundaries are recorded in [release evidence](docs/development/version-2.md); reports for released and development code are welcome. +Version 2.0.5 includes the reviewed safety and schema-3 enrollment changes and the configurable central-retention grace fix. Supported scope and historical release boundaries are recorded in [release evidence](docs/development/version-2.md); reports for released and development code are welcome. Security fixes are prepared for the latest published release. The project does not promise backports to older versions. diff --git a/SUPPORT.md b/SUPPORT.md index 904a66c..e80c089 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -4,6 +4,6 @@ Use [GitHub Issues](https://github.com/CruxExperts/BestBackup/issues) for bugs a For a bug report, include the bbackup version, Linux distribution, Python version, command, storage type, and a short description of the expected and observed behavior. Remove passwords, access keys, repository URLs with credentials, personal data, and sensitive file paths before posting configuration or output. Never attach host bindings or credential files. -Version 2.0.3 is supported only for its documented tested paths, including the bounded user-level scheduled file-backup guest drills. B2/AWS provider protection and independent host-loss recovery remain unqualified; see [release evidence and limits](docs/development/version-2.md). For development builds, include the commit and policy schema version. +Version 2.0.5 is supported only for its documented tested paths, including the bounded user-level scheduled file-backup guest drills. B2/AWS provider protection and independent host-loss recovery remain unqualified; see [release evidence and limits](docs/development/version-2.md). For development builds, include the commit and policy schema version. For a security vulnerability, do not open a public issue. Follow the private process in [SECURITY.md](SECURITY.md). diff --git a/VERSION b/VERSION index 2165f8f..e010258 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.0.4 +2.0.5 diff --git a/bbackup/__init__.py b/bbackup/__init__.py index 665ff82..31f32e6 100644 --- a/bbackup/__init__.py +++ b/bbackup/__init__.py @@ -3,5 +3,5 @@ Encrypted restic-based workflows; legacy Docker commands remain available. """ -__version__ = "2.0.4" +__version__ = "2.0.5" __author__ = "Slavic Kozyuk / Crux Experts LLC" diff --git a/bbackup/cloud.py b/bbackup/cloud.py index b2ee1a4..d34c43b 100644 --- a/bbackup/cloud.py +++ b/bbackup/cloud.py @@ -1227,7 +1227,6 @@ def reconcile_central_retention( "mutation_outcome_uncertain", "policy_or_grant_expired", ] -_MIN_RETENTION_GRACE_SECONDS = 30 * 24 * 60 * 60 _PRIVATE_JSON_LIMIT = 16 * 1024 * 1024 _RETENTION_PLAN_FIELDS = { "schema_version", @@ -1658,7 +1657,7 @@ def _validate_retention_policy( if any(type(counts[name]) is not int or counts[name] < 0 for name in counts): raise CloudError("Central retention checkpoint counts are invalid") for name, minimum in ( - ("grace_seconds", _MIN_RETENTION_GRACE_SECONDS), + ("grace_seconds", 1), ("plan_max_age_seconds", 1), ("max_apply_objects", 1), ("max_apply_bytes", 1), diff --git a/bbackup/data/cli-skills.md b/bbackup/data/cli-skills.md index 04b74f6..c13d983 100644 --- a/bbackup/data/cli-skills.md +++ b/bbackup/data/cli-skills.md @@ -1,6 +1,6 @@ # CLI skills catalog -> Generated from the bbackup/bbman CLI metadata. Version: 2.0.4. This catalog is authoritative for this version. +> Generated from the bbackup/bbman CLI metadata. Version: 2.0.5. This catalog is authoritative for this version. > Source: `bbackup/cli_metadata.py` rendered by `scripts/generate_cli_skills.py`; run the generator's `--check` mode before publishing. ## bbackup diff --git a/docs/PUBLISHING_CHECKLIST.md b/docs/PUBLISHING_CHECKLIST.md index ce6dc10..6ef88d3 100644 --- a/docs/PUBLISHING_CHECKLIST.md +++ b/docs/PUBLISHING_CHECKLIST.md @@ -24,8 +24,8 @@ Use this checklist before changing public project metadata or publishing a relea ## Version 2 tested-scope claims -- [ ] Describe v2.0.4 as qualified only for local file capture/restore, live PostgreSQL 17.11 / MySQL 8.4.12 / MariaDB 11.8.9 capture/restore, headless dashboard happy paths, and AMD64 Ubuntu 24.04/26.04 packages; this is the v2.0.3 tested core scope. -- [ ] Describe new v2.0.4 cloud and Linux workflows as experimental and unqualified; keep requests #23–#26 open and do not claim a guaranteed thirty-day window, off-host custody, final remote-server qualification, or complete-machine recovery. +- [ ] Describe v2.0.5 as qualified only for local file capture/restore, live PostgreSQL 17.11 / MySQL 8.4.12 / MariaDB 11.8.9 capture/restore, headless dashboard happy paths, and AMD64 Ubuntu 24.04/26.04 packages; this scope is unchanged from v2.0.4. +- [ ] Describe new v2.0.5 cloud and Linux workflows as experimental and unqualified; issues #23–#26 were canceled and closed as not planned. This release adds no new live provider qualification. Do not claim a guaranteed thirty-day window, off-host custody, final remote-server qualification, or complete-machine recovery. - [ ] Keep B2/AWS, native Docker, ARM64 backup/restore, cross-application-version upgrades/rollback, root-owned systemd package templates, and cloud/off-host protection explicitly unqualified; describe the separate user-timer guest drills with their exact limits. - [ ] State that ARM64 packages may be built by the release matrix but are not functionally qualified by package-install smoke alone. - [ ] Keep native B2 capability inspection distinct from backup transfer; inspection is read-only and always reports `protection_qualified: false`. diff --git a/docs/README.md b/docs/README.md index a69d0f5..55f9391 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,7 +2,7 @@ bbackup is for Linux administrators, homelab and self-hosted operators, and small infrastructure teams who need encrypted local backups, verified file restores, and visible scheduled jobs. Choose a guide by the work you are doing. -These guides describe v2.0.4. The [qualification record](development/version-2.md) is the authority for the exact qualified scope, version-specific evidence, and remaining limits; an implemented path alone does not establish support. The v2.0.4 release retains the v2.0.3 tested core scope, while new cloud and Linux workflows are experimental and unqualified. Operators still using v2.0.2 can follow its [versioned documentation](https://github.com/CruxExperts/BestBackup/tree/v2.0.2/docs) until they migrate. +These guides describe v2.0.5. The [qualification record](development/version-2.md) is the authority for the exact qualified scope, version-specific evidence, and remaining limits; an implemented path alone does not establish support. The v2.0.5 release retains the v2.0.4 tested core scope, while new cloud and Linux workflows are experimental and unqualified. Operators still using v2.0.2 can follow its [versioned documentation](https://github.com/CruxExperts/BestBackup/tree/v2.0.2/docs) until they migrate. The [release dependency evidence](development/release-dependencies.json) records the reviewed versions, source timestamps, lock hash, and audit limits. @@ -14,7 +14,7 @@ The [release dependency evidence](development/release-dependencies.json) records | Migrate an existing schema-2 setup | [Quick start: migrate an existing configuration](../QUICKSTART.md#migrate-an-existing-schema-2-configuration) | | Maintain an older Docker/YAML workflow | [Legacy management](management.md) and [encryption compatibility](encryption.md) | | Schedule jobs or integrate with scripts and agents | [Quick start: apply a schedule](../QUICKSTART.md#apply-a-schedule-deliberately), [agent integration](AGENT_INTEGRATION.md), and the [production command reference](production-cli.md) | -| Check what v2.0.4 qualifies | [Version 2 qualification record](development/version-2.md) | +| Check v2.0.5 scope and issue status | [Version 2 qualification record](development/version-2.md) (issues #23–#26 were canceled and closed as not planned; they remain unqualified) | | Evaluate experimental cloud recovery, retention or Linux coverage | [Experimental program and limits](development/version-2.md#experimental-cloud-and-machine-recovery-features) and [cloud storage](cloud-storage.md) | ## Operator references diff --git a/docs/VERSIONING.md b/docs/VERSIONING.md index d2db1aa..bb6fce0 100644 --- a/docs/VERSIONING.md +++ b/docs/VERSIONING.md @@ -1,6 +1,6 @@ # Versioning -`VERSION` is the canonical release version. The current version is `2.0.4`. +`VERSION` is the canonical release version. The current version is `2.0.5`. The earlier `v2.0.0` and `v2.0.1` tags did not produce release artifacts; the retained legacy line is `1.8.6`. Stable support is limited to the tested scope in [`docs/development/version-2.md`](development/version-2.md). @@ -17,7 +17,7 @@ Release-facing references must match it before publishing: The release workflow accepts alpha, beta, and release-candidate suffixes. A prerelease tag such as `v2.0.0-alpha.1` is marked as a GitHub prerelease; a -stable tag such as `v2.0.4` is not. Every tag must match `VERSION`, its dated +stable tag such as `v2.0.5` is not. Every tag must match `VERSION`, its dated changelog section, and generated schemas before release validation. Normal release batches use a patch bump by default, including commits with diff --git a/docs/cli-skills.md b/docs/cli-skills.md index 04b74f6..c13d983 100644 --- a/docs/cli-skills.md +++ b/docs/cli-skills.md @@ -1,6 +1,6 @@ # CLI skills catalog -> Generated from the bbackup/bbman CLI metadata. Version: 2.0.4. This catalog is authoritative for this version. +> Generated from the bbackup/bbman CLI metadata. Version: 2.0.5. This catalog is authoritative for this version. > Source: `bbackup/cli_metadata.py` rendered by `scripts/generate_cli_skills.py`; run the generator's `--check` mode before publishing. ## bbackup diff --git a/docs/cloud-storage.md b/docs/cloud-storage.md index d7aed2b..3ee3fe5 100644 --- a/docs/cloud-storage.md +++ b/docs/cloud-storage.md @@ -1,6 +1,6 @@ # Cloud storage: Backblaze B2 and Amazon S3 -Keep your first recovery point on local storage. The v2.0.4 tested stable-release +Keep your first recovery point on local storage. The v2.0.5 tested stable-release scope retains local/file capture and restore, selected live database drills, headless dashboard happy paths, and AMD64 Ubuntu packages. @@ -9,11 +9,13 @@ small live B2 source-path drill passed on 2026-10-20; it does not qualify B2 protection or production recovery. No live AWS capture/check/restore result is recorded. The local Garage S3 test also passed capture, copy, full-data checking, and restore, but does not qualify B2 or Amazon S3. The provider workflows ship -as experimental in v2.0.4; keep B2/AWS outside production recovery until the +as experimental in v2.0.5; keep B2/AWS outside production recovery until the remaining provider and independent-recovery evidence is complete. A separate live B2 lifecycle-overlap guard confirmed only the configuration check and central retention refusal; it did not test elapsed expiry or physical deletion. See the [guard evidence](#b2-lifecycle-overlap-guard-experimental). +This release adds no new live provider qualification; existing workstation +trials remain historical and unqualified. ## Choose a connection @@ -80,9 +82,11 @@ directory-scoped machine prefix, repository read/list/write capabilities, and no permanent-version deletion, administration, or unknown capabilities. A broad key can still be inspected, but does not satisfy this profile. An eligible metadata result alone does not prove that a forbidden operation is denied. The -small live trial below passed seven denial checks, but full recovery -qualification remains required by -[issue #23](https://github.com/CruxExperts/BestBackup/issues/23). +small live trial below passed seven denial checks. +[Issue #23](https://github.com/CruxExperts/BestBackup/issues/23) was canceled and +closed as not planned at the user's request. Full B2 recovery qualification +remains unproven; closing the request does not change the supported scope or +establish independent escrow custody or a guaranteed historical recovery window. Use a distinct prefix per machine, such as `machines/MACHINE_ID/`. Keep the provisioner, central verifier, and retention executor identities off source @@ -148,7 +152,9 @@ versions only**. They do not run Restic `forget` or `prune`, compact packs, or delete current objects and delete markers. Keeping all current packs avoids breaking deduplicated snapshots. Full repository compaction and live provider qualification remain outside this implementation slice of -[issue #25](https://github.com/CruxExperts/BestBackup/issues/25). +[issue #25](https://github.com/CruxExperts/BestBackup/issues/25), which was +subsequently canceled and closed as not planned. The validated configurable-grace +fix is retained; no new live provider qualification is claimed. Run retention on the independently controlled management host with its own deletion-capable credentials. Use an enrolled schema-3 repository binding whose @@ -161,7 +167,8 @@ The mode-0600 policy remains schema 1 for generic S3. It contains `policy_id`, a 64-character random hex `catalog_id`, positive `revision`, exact `endpoint_url`, `bucket`, nonempty `prefix`, full `repository_id`, trusted central verifier primary fingerprint, `retention_counts` (`daily`, `weekly`, `monthly`), -`grace_seconds` (at least 2592000), `plan_max_age_seconds`, +`grace_seconds` (positive integer; 2592000 seconds, or 30 days, is +recommended), `plan_max_age_seconds`, `max_apply_objects`, `max_apply_bytes`, private absolute `state_dir`, nonempty `known_good_manifest_digests`, and `pending_manifest_digests`. Keep central state independently backed up. Losing an established catalog stops planning; @@ -232,9 +239,16 @@ bbackup production --config config.v3.json --bindings bindings.v3.json \ Catalog enrollment is a one-time action. Inspect the plan's sanitized counts and digests and private immutable file before applying its exact ID and SHA-256. -The noncurrent grace clock starts at central first observation, rather than the -upload timestamp. Enabled lifecycle expiration rules are refused. Versioning, -bucket locking, per-version retention and legal holds are checked conservatively. +The v2.0.5 configurable-grace change accepts any positive integer; v2.0.4 +required at least 2592000 seconds. The configured noncurrent grace clock starts +at central first observation, +rather than the upload timestamp; eligibility begins when the configured number +of seconds has elapsed. The value is policy-controlled, with 30 days recommended +as a conservative baseline. A shorter interval also shortens grace-based +protection for accepted historical checkpoints; a short fixture checks only the +configured boundary and does not establish a thirty-day recovery guarantee. +Enabled lifecycle expiration rules are refused. Versioning, bucket locking, +per-version retention and legal holds are checked conservatively. The live verifier read succeeded after the separate central identity received `readBucketLifecycleRules`; versioning was enabled and no lifecycle expiration rules applied. No lifecycle-write capability was added. Separate canaries diff --git a/docs/development/release-dependencies.json b/docs/development/release-dependencies.json index 175a33a..763e6df 100644 --- a/docs/development/release-dependencies.json +++ b/docs/development/release-dependencies.json @@ -16,58 +16,58 @@ ], "package_version_coverage_verified_against_lockfile": true, "query_failures": 0, - "raw_evidence_sha256": "1c04489e2becf51c2ed9d2acea18bfbbc2db210c9f5a2d331c53ec7263eaf3b8", + "raw_evidence_sha256": "d976f5d4c39ab2d1dbfc55448ced4f2bb8696394500f166c0f27982b49803846", "result": "No advisory records were returned for these exact package versions by OSV, GitHub Advisory Database, deps.dev, or PyPI at retrieval time.", - "retrieved_at_utc": "2026-10-21T09:12:55.211479+00:00", + "retrieved_at_utc": "2026-10-22T03:13:27.879942+00:00", "scope": "All 51 exact PyPI registry package versions in uv.lock, plus the exact setuptools and wheel versions resolved for the build-system requirements.", "sources": [ { "advisory_hits": 0, "endpoint": "https://api.osv.dev/v1/querybatch", "http_status_counts": { - "200": 2 + "200": 1 }, "independence_class": "A", "name": "OSV", "package_version_queries": 53, - "requests": 2, - "retrieved_at_utc": "2026-10-21T09:12:55.211479+00:00" + "requests": 1, + "retrieved_at_utc": "2026-10-22T03:13:27.879942+00:00" }, { "advisory_hits": 0, "endpoint": "https://api.github.com/advisories?ecosystem=pip&affects=PACKAGE%40VERSION", "http_status_counts": { - "200": 54 + "200": 6 }, "independence_class": "B", "name": "GitHub Advisory Database", "package_version_queries": 53, - "retrieved_at_utc": "2026-10-21T09:12:55.211479+00:00", - "requests": 54 + "retrieved_at_utc": "2026-10-22T03:13:27.879942+00:00", + "requests": 6 }, { "advisory_hits": 0, "endpoint": "https://api.deps.dev/v3/systems/pypi/packages/PACKAGE/versions/VERSION", "http_status_counts": { - "200": 54 + "200": 53 }, "independence_class": "E", "name": "deps.dev", "package_version_queries": 53, - "retrieved_at_utc": "2026-10-21T09:12:55.211479+00:00", - "requests": 54 + "retrieved_at_utc": "2026-10-22T03:13:27.879942+00:00", + "requests": 53 }, { "advisory_hits": 0, "endpoint": "https://pypi.org/pypi/PACKAGE/VERSION/json", "http_status_counts": { - "200": 54 + "200": 53 }, "independence_class": "E", "name": "PyPI JSON API", "package_version_queries": 53, - "retrieved_at_utc": "2026-10-21T09:12:55.211479+00:00", - "requests": 54 + "retrieved_at_utc": "2026-10-22T03:13:27.879942+00:00", + "requests": 53 } ], "remediation": { @@ -80,10 +80,10 @@ } }, "lockfile": { - "local_project_version": "2.0.4", + "local_project_version": "2.0.5", "locked_pypi_packages": 51, "path": "uv.lock", - "sha256": "d1163858fc14e6aa811e96ca6ae2136def3482b681bfe08434254b112203d6e9" + "sha256": "b96e18305ab40567f2df3894c29d05b32809ed3b5f344fb2a7f72d03ca2e2269" }, "package_versions": [ { @@ -300,8 +300,8 @@ } ], "release": { - "release_date_utc": "2026-10-21", - "version": "2.0.4" + "release_date_utc": "2026-10-22", + "version": "2.0.5" }, "schema_version": 1, "external_build_tools": { @@ -327,7 +327,8 @@ "advisory": "GHSA-hq85-5cpj-mr62", "result": "No advisory records returned for the exact uv 0.13.4 version by OSV, GitHub Advisory Database, deps.dev, or the PyPI JSON API.", "exact_version_only": true, - "raw_evidence_sha256": "d5f443900b224786589136f8447b405d25142b2355790f5bb97e0c3ec1082135" + "raw_evidence_sha256": "8ef56a29304e58e5ac937a498eece98dbcab380a59b07362f3b60ee3a12eba85", + "retrieved_at_utc": "2026-10-22T03:14:58.250134+00:00" } }, "restic": { diff --git a/docs/development/version-2.md b/docs/development/version-2.md index f92da3e..6a6a720 100644 --- a/docs/development/version-2.md +++ b/docs/development/version-2.md @@ -1,10 +1,27 @@ # Version 2 development -> Stable v2.0.4 production qualification retains the tested core scope recorded -> below. Experimental additions remain unqualified. The 2026-10-17 record keeps -> the original v2.0.3 evidence and limits. +> Stable v2.0.5 production qualification retains the tested core scope recorded +> below. Experimental additions remain unqualified. The dated records retain +> historical evidence and limits without expanding that scope. -## v2.0.4 release scope and known limits +## v2.0.5 release scope and known limits + +The v2.0.5 stable qualified scope is unchanged from v2.0.4: local file capture +and restore; live capture and restore for PostgreSQL 17.11, MySQL 8.4.12, and +MariaDB 11.8.9; headless dashboard happy paths; and AMD64 Ubuntu 24.04/26.04 +packages. The separate user-timer reboot qualification remains limited to the +components and conditions recorded in its dated evidence below. + +This patch accepts a positive, explicitly configured `grace_seconds` value for +experimental central retention; thirty days remains the recommended baseline. +Shorter grace reduces the grace-based historical protection period and does not +establish a thirty-day recovery guarantee. B2/AWS protection, aged reclamation, +off-host custody, and production recovery remain unqualified. Issues #23–#26 +were canceled and closed as not planned; cancellation does not qualify their +experimental workflows. This release adds no new live provider qualification; +the historical workstation evidence below remains unchanged. + +## v2.0.4 release scope and known limits (historical) The v2.0.4 stable qualified scope is unchanged from v2.0.3: local file capture and restore; live capture and restore for PostgreSQL 17.11, MySQL 8.4.12, and @@ -20,7 +37,8 @@ path. These additions are implemented workflows, not new production support. They do not establish B2 or AWS protection, a guaranteed thirty-day recovery window, off-host custody, final remote-server qualification, complete-machine recovery, or general automated PostgreSQL coverage. `protection_qualified` -remains false. Requests #23–#26 remain open; this release does not close them. +remains false. Issues #23–#26 were canceled and closed as not planned; their experimental +workflows remain unqualified. This release does not expand the tested support scope. On 2026-10-21, the v2.0.4 candidate passed the full local suite with 1,242 passed tests and one skipped test. The exact wheel and source archive built @@ -33,7 +51,7 @@ These checks do not expand the production qualification boundary above. ## Direction and delivery order -The selected stable v2.0.4 release scope is local/file capture and restore, +The v2.0.4 stable release scope, retained in v2.0.5, is local/file capture and restore, live capture-restore drills for PostgreSQL 17.11, MySQL 8.4.12, and MariaDB 11.8.9, headless dashboard happy paths, and AMD64 Ubuntu 24.04/26.04 packages. The CLI and Textual dashboard share one operations service. Existing 1.x commands @@ -54,18 +72,21 @@ An implemented capability does not establish completion of its entire delivery s ### Experimental cloud and machine recovery features -Open requests are sequenced as restricted B2 recovery -([#23](https://github.com/CruxExperts/BestBackup/issues/23)), central retention -([#25](https://github.com/CruxExperts/BestBackup/issues/25)), remote-primary disk -budgets ([#26](https://github.com/CruxExperts/BestBackup/issues/26)), and integrated -privileged Linux recovery ([#24](https://github.com/CruxExperts/BestBackup/issues/24)). -Linux coverage discovery can proceed independently. Each request needs its own -implementation, focused regressions, documentation, review, signed commit, and -acceptance evidence before closeout. Stable qualification and issue closeout -require completion and final source/artifact validation; this release ships the -current workflows experimentally without closing the requests. A mocked -provider result cannot qualify B2. All four requests remain open; the live -trials below do not close them. +Issue [#23](https://github.com/CruxExperts/BestBackup/issues/23), for restricted +B2 recovery, was canceled and closed as not planned. The implementation and live +trials remain experimental and do not qualify B2 protection. Central retention +([#25](https://github.com/CruxExperts/BestBackup/issues/25)) was also canceled and +closed as not planned; its validated configurable-grace fix is retained. +Remote-primary disk budgets +([#26](https://github.com/CruxExperts/BestBackup/issues/26)) were also canceled and +closed as not planned, retaining the existing experimental implementation. +Integrated privileged Linux recovery +([#24](https://github.com/CruxExperts/BestBackup/issues/24)) was likewise canceled +and closed as not planned. All four requests are now closed by cancellation, +not qualification. Existing implementations and historical evidence are retained. +Any future qualification requires focused regressions, independent review and +final source/artifact validation against the claimed scope. A mocked provider +result cannot qualify B2; the live trials below do not establish new support. The experimental B2 slice adds a restricted source-key metadata assessment to native B2 inspection and an independent `recovery accept` command. Acceptance @@ -333,7 +354,7 @@ SQLite/files drill restores committed rows, omits uncommitted rows and raw database/journal files, validates export provenance, and reconstructs original metadata with the reviewed recipe. Privileged complete-machine recovery and final remote-server qualification remain -outstanding for #24. +unproven; #24 was subsequently canceled and closed as not planned. On 2026-10-21, the opt-in `covered-root` scenario in [`qualify_systemd.py`](../../scripts/qualify_systemd.py) passed a separate @@ -414,6 +435,36 @@ Healthy reconciliation alone does not certify completed prune. Repacking, aged live deletion and the promised recovery window remain unqualified. See the [central maintenance workflow](../cloud-storage.md#central-snapshot-maintenance-experimental). +#### Issue #25 configurable grace (v2.0.5) + +The v2.0.5 central retention policy accepts a positive `grace_seconds` +value; thirty days is the recommended baseline, not a hard-coded minimum. +Eligibility is measured from central observation of a noncurrent version rather than its +upload timestamp. The recent live trials do not establish an elapsed configured +grace interval. Their zero-forget maintenance, stale-plan rejection and +interruption results do not demonstrate aged permanent deletion or physical +reclamation. + +[Issue #25](https://github.com/CruxExperts/BestBackup/issues/25) was canceled and +closed as not planned at the user's request. Any future qualification must +preserve the original observation catalog and accepted recovery dependencies +through the policy's configured grace period. Then use an exact-target +authorized plan to exercise eligible aged B2 versions, verify locked and held +versions survive, and restore the retained recovery points. Record the exact +source revision, policy and grant revisions, plan identity, provider +observations and outcomes without publishing credentials. Do not backdate +observations, bypass provider locks, or replay an uncertain deletion to +manufacture acceptance evidence. No new live deletion or recovery result is +claimed by this review. + +The configurable-grace candidate passed the full local suite on 2026-10-22 UTC +with 1,253 passes and one skip using three workers. The focused retention file +contains 59 tests, including actual apply at the configured observation boundary +and held-version rejection. Ruff, version synchronization, publishing readiness, +Markdown standards and generated CLI checks passed. Independent review found no +material issue with policy/grant binding or the existing deletion safeguards. +These checks do not establish live B2 reclamation or provider qualification. + Local validation includes a real three-snapshot Restic repository: the exact obsolete accepted ID was forgotten, no-repack prune completed, and both the protected and unknown snapshots restored afterward. That drill uses a test @@ -468,7 +519,8 @@ catalog digest was unchanged. The eight-byte probe was deliberately retained. This qualifies only the small, zero-forget maintenance exercise. It does not demonstrate aged reclamation, an elapsed protection window, independently held off-host escrow, or final remote-server recovery. `protection_qualified` remains -false, the feature ships experimentally in v2.0.4, and #25 remains open. +false. The feature ships experimentally in v2.0.4; #25 was subsequently +closed as not planned without expanding qualification. #### Live B2 central-maintenance interruption trial @@ -659,8 +711,10 @@ was updated, and no deletion was attempted. This establishes only the configured overlap guard and central-retention refusal. It does not test elapsed lifecycle expiry, physical reclamation, or any file-level lock/retention window; no actual file objects were created. -Central acceptance remains `protection_qualified: false`, requests #23–#26 -remain open, and the v2.0.4 stable scope remains the v2.0.3 tested core scope. +Central acceptance remains `protection_qualified: false`. At the time of this +2026-10-21 trial, requests #23–#26 were open; #23 has since been closed as not +planned, as have #24–#26. Their closure records cancellation, not qualification. The v2.0.4 stable scope remains the v2.0.3 +tested core scope. On 2026-10-20 a workstation-only Garage S3 drill captured a 2,184,000-byte file fixture directly to the emulator using a 1,048,576-byte staging mount, ran an @@ -710,7 +764,8 @@ This qualifies only the small local scenario. It does not measure normal peak storage, incompressible-data scale, host memory, persistent staging across reboot, off-host recovery, or production recovery timing. Reserve checks remain observed thresholds rather than a hard guarantee. Final remote-server -qualification for #26 remains deferred. +qualification for #26 was subsequently canceled; the issue is closed as not +planned and the experimental support limit is unchanged. ### Workstation implementation checks - 2026-10-20 diff --git a/docs/production-cli.md b/docs/production-cli.md index b8f62cc..a341bbc 100644 --- a/docs/production-cli.md +++ b/docs/production-cli.md @@ -2,7 +2,7 @@ > Generated by `scripts/generate_cli_skills.py` from `bbackup/cli_metadata.py`. -The command contract is stable in v2.0.4. Production qualification remains limited to the tested scope in [version 2 release evidence](development/version-2.md). +The command contract is stable in v2.0.5. Production qualification remains limited to the tested scope in [version 2 release evidence](development/version-2.md). Prefix commands with `bbackup production --config CONFIG --bindings BINDINGS`. diff --git a/pyproject.toml b/pyproject.toml index cca903c..3a4fca0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "bbackup" -version = "2.0.4" +version = "2.0.5" description = "Local-first encrypted Linux backups with restic and a visual dashboard" readme = "README.md" requires-python = ">=3.12" diff --git a/tests/test_cloud_retention.py b/tests/test_cloud_retention.py index 00b1d14..a65abf8 100644 --- a/tests/test_cloud_retention.py +++ b/tests/test_cloud_retention.py @@ -331,6 +331,13 @@ def _refresh_grant_for_policy(env): _write_json(env["grant_path"], env["grant"]) +def _set_grace_seconds(env, seconds): + env["policy"] = dict(env["policy"]) + env["policy"]["grace_seconds"] = seconds + _write_json(env["policy_path"], env["policy"]) + _refresh_grant_for_policy(env) + + def _prepare_maintenance(env, *, max_objects=20, max_bytes=1024): second_dir, _manifest, _digest, _acceptance = _make_record( env["root"] / "maintenance-old-acceptance", @@ -585,6 +592,82 @@ def test_recently_hidden_old_upload_starts_grace_now_and_last_modified_is_ignore assert env["store"].deleted == [] +@pytest.mark.parametrize("grace_seconds", [1, 60]) +def test_retention_policy_accepts_positive_configured_grace(retention_env, grace_seconds): + env = retention_env + policy = {**env["policy"], "grace_seconds": grace_seconds} + + cleaned, _state_dir = cloud._validate_retention_policy(policy, env["adapter"]) + + assert cleaned["grace_seconds"] == grace_seconds + + +@pytest.mark.parametrize("grace_seconds", [0, -1, True, False, 1.5, "60"]) +def test_retention_policy_rejects_nonpositive_or_noninteger_grace(retention_env, grace_seconds): + env = retention_env + policy = {**env["policy"], "grace_seconds": grace_seconds} + + with pytest.raises(CloudError, match="limits are invalid"): + cloud._validate_retention_policy(policy, env["adapter"]) + + +def test_short_configured_grace_eligibility_begins_at_exact_observation_boundary(retention_env): + env = retention_env + _set_grace_seconds(env, 60) + first = _plan(env) + assert first["candidate_count"] == 0 + + FrozenDateTime.current += timedelta(seconds=59) + before_boundary = _plan(env) + assert before_boundary["candidate_count"] == 0 + + FrozenDateTime.current += timedelta(seconds=1) + at_boundary = _plan(env) + assert at_boundary["candidate_count"] == 1 + private_plan = json.loads( + (env["state"] / "retention-plans" / f"{at_boundary['operation_id']}.json").read_text() + ) + assert [(item["key"], item["version_id"]) for item in private_plan["candidates"]] == [ + ("repo/other/old.bin", "old-v1") + ] + assert ("repo/data/restore.bin", "historical-v1", False, 8) in env["store"].versions + assert ("repo/data/restore.bin", "current-v2", True, 11) in env["store"].versions + assert ("repo/other/old.bin", "current-v2", True, 5) in env["store"].versions + + result = _apply(env, at_boundary) + + assert result["status"] == "complete" + assert env["store"].deleted == [("repo/other/old.bin", "old-v1")] + assert ("repo/data/restore.bin", "historical-v1", False, 8) in env["store"].versions + assert ("repo/data/restore.bin", "current-v2", True, 11) in env["store"].versions + assert ("repo/other/old.bin", "current-v2", True, 5) in env["store"].versions + + +@pytest.mark.parametrize("lock_state", ["legal_hold", "governance"]) +def test_short_configured_grace_keeps_held_noncurrent_versions_ineligible(retention_env, lock_state): + env = retention_env + _set_grace_seconds(env, 60) + _plan(env) + FrozenDateTime.current += timedelta(seconds=60) + candidate = ("repo/other/old.bin", "old-v1") + if lock_state == "legal_hold": + env["store"].legal_holds[candidate] = "ON" + else: + retain_until = FrozenDateTime.current + timedelta(seconds=60) + env["store"].retention[candidate] = { + "Mode": "GOVERNANCE", + "RetainUntilDate": FrozenDateTime.fromtimestamp( + retain_until.timestamp(), timezone.utc, + ), + } + + plan = _plan(env) + + assert plan["candidate_count"] == 0 + assert plan["held_version_count"] == 1 + assert env["store"].deleted == [] + + def test_missing_catalog_record_and_reaccepted_timestamp_fail_closed(retention_env): env = retention_env second_dir, _second_manifest, _second_digest, _second_acceptance = _make_record( diff --git a/uv.lock b/uv.lock index 4451f5f..7ea5165 100644 --- a/uv.lock +++ b/uv.lock @@ -31,7 +31,7 @@ wheels = [ [[package]] name = "bbackup" -version = "2.0.4" +version = "2.0.5" source = { editable = "." } dependencies = [ { name = "b2sdk" },