From 72f51299e5d86cb67e06e26c4b5d1aa6faafb4ca Mon Sep 17 00:00:00 2001 From: owenpkent <20529132+owenpkent@users.noreply.github.com> Date: Mon, 21 Sep 2026 19:19:58 -0400 Subject: [PATCH 1/7] Bring the docs back in line with what the checkout does A sweep after the update client, the candidate workflow and the bill of materials landed. Every change here is either a claim that had stopped being true or a thing the checkout does that nothing documented. Two statements were actively wrong, both falsified by the SBOM work: that the build inventories are "not a complete third-party license inventory or SBOM", and that a release-ready inventory and SBOM "remain separate work". The release plan still said the in-app check was deferred, and its scope freeze still listed auto-update flatly. That one needed splitting rather than deleting: the check is automatic, installing never is, and a silent background update is still out of scope, which is a more useful sentence than either "deferred" or nothing. The README listed `update` in the commands table and explained it nowhere. It now has a section covering what is verified before anything runs and, more importantly, that an update never closes a running Offloader, which is the guarantee rather than a limitation. The documentation index gains updates.md. The agent guide's code map listed none of the installation or update modules, so a reader looking for where maintenance lives had to grep. It now names installation.py, installation_lock.py, update.py and gui/updates.py together with the rule that binds them: neither may force-close a running transfer. Its start-here list points at build-windows.md and updates.md, and says that a build being implemented is not a release being possible, since that is the distinction the plan keeps having to restate. The roadmap still listed a Windows installer and code signing as future work when the packaging is implemented. Replaced with what is actually left, none of which is code: the hardware token, a clean machine, and the Qt LGPL/GPL decision the notices file flags on every build. Added a dated source-validation record, labelled as exactly that. Nothing was frozen, signed or installed in it, so it establishes none of the gates it sits above. Checked rather than assumed: all 68 relative links across the markdown docs resolve, every CLI subcommand appears in the commands table, and every `offload` flag is mentioned in the README. --- AGENTS.md | 7 +++++++ README.md | 28 +++++++++++++++++++++++++++- ROADMAP.md | 9 +++++++-- docs/build-windows.md | 14 +++++++++++--- docs/release-plan.md | 19 ++++++++++++------- 5 files changed, 64 insertions(+), 13 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 48bdb49..f2d8532 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,6 +18,12 @@ takes priority over features, throughput, and presentation. [docs/release-plan.md](docs/release-plan.md) for the proposed Windows beta. The release plan describes future work; verify the checkout before claiming an installer, signing pipeline, or release gate exists. +- Packaging, signing, the bill of materials and the tag-triggered candidate + workflow are in [docs/build-windows.md](docs/build-windows.md); what an + update checks before it runs anything is in + [docs/updates.md](docs/updates.md). Hardware-key signing and clean-machine + qualification are still gates, so a build being implemented is not a + release being possible. - Inspect the working tree before edits. Preserve unrelated user changes. Keep this guide concise and link to detailed documentation rather than copying it wholesale. @@ -56,6 +62,7 @@ All module paths below are relative to `src/offloader/`. | Timeline import | `timeline.py`: optional OpenTimelineIO integration, media resolution, and ambiguity handling | | Desktop | `gui/main_window.py`, `gui/worker.py`, `gui/queue_view.py`, mode/editor widgets, and `gui/drives.py` | | Persistent state | `config.py`, `presets.py`, `history.py`; `volumes.py` discovers storage and `naming.py` handles naming | +| Installation and updates | `installation.py` and `installation_lock.py` for transactional maintenance and the shared installed-instance lock; `update.py` finds, verifies and hands over a release, wrapped for the app by `gui/updates.py`. Neither may force-close a running transfer; see [docs/updates.md](docs/updates.md) | Python 3.10+ is supported. Core dependencies are xxhash and ReportLab; PySide6 is the GUI extra. Timeline dependencies are separate extras. ffmpeg/ffprobe diff --git a/README.md b/README.md index ac1646d..321d8d4 100644 --- a/README.md +++ b/README.md @@ -230,6 +230,31 @@ manifest lists and exits non-zero if anything is off, so a format script can gat on it. `--allow-cache` skips the page-cache eviction — faster, and may verify memory rather than the device. +### `update` + +```sh +offloader update # is there a newer release? +offloader update --install # download it, verify it, run the installer +``` + +GitHub Releases is the feed, so there is no manifest server and no second +place a version number is recorded. Before anything runs, the download has to +be served from an allowlisted host *after* redirects, hash to what was +computed while streaming, and carry a valid Authenticode signature with this +project's certificate thumbprint, its publisher name, and an embedded +`FileVersion` matching the release. That last check is what stops an older +but still validly signed installer being re-served under a newer asset name. + +**It never closes a running Offloader.** The installer refuses maintenance +while a transfer is in flight, which is the guarantee rather than a +limitation, so updating means finishing or cancelling the job first. The +command says so before the elevation prompt appears. + +The desktop app does the same thing from **Help, Check for updates now**, and +checks once on launch unless that is turned off in Options. Full detail, +including what is deliberately not copied from the reference implementation, +is in [`docs/updates.md`](docs/updates.md). + ### Verification modes | Mode | What it does | Catches | @@ -516,7 +541,8 @@ general-purpose tool reports a filename, a size, and a placeholder icon. | --- | --- | | [`ROADMAP.md`](ROADMAP.md) | What is next, why, and what this will not become | | [`docs/release-plan.md`](docs/release-plan.md) | Windows beta release sequence, packaging, signing, acceptance gates, and recovery | -| [`docs/build-windows.md`](docs/build-windows.md) | Build, sign, and check Windows desktop bundles and installers | +| [`docs/build-windows.md`](docs/build-windows.md) | Build, sign, and check Windows desktop bundles and installers; the bill of materials, and tagging a candidate | +| [`docs/updates.md`](docs/updates.md) | What an update checks before it runs anything, and why it never closes a running transfer | | [`docs/data-safety.md`](docs/data-safety.md) | Threat model: what is guaranteed, what is not, and the bugs behind each guarantee | | [`docs/report-layout.md`](docs/report-layout.md) | Every coordinate of the PDF, measured off the reference report | | [`docs/performance.md`](docs/performance.md) | Why not robocopy, with benchmarks and the confounds that made the first run worthless | diff --git a/ROADMAP.md b/ROADMAP.md index c3c13d9..587a297 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -127,8 +127,13 @@ already does the stem-matching this needs. it settles almost every case for a stat, then the checksum. What is missing is only the wiring into the engine's own skip, where the file being skipped is at the destination rather than under another search root. -- **Windows installer and code signing**, so it can be handed to someone who - does not have Python. +- **A signed, qualified Windows release.** The packaging is implemented: + frozen bundle, NSIS installer, transactional maintenance, signing hooks, a + bill of materials, an update client, and a tag-triggered candidate workflow. + What is left is not code. Signing needs the hardware token, the acceptance + matrix needs a clean machine, and Qt's LGPL/GPL terms against Offloader's + MIT licence need a redistribution decision, which the notices file flags on + every build. See [`docs/release-plan.md`](docs/release-plan.md). - **Per-job report templates and custom branding.** ## Not planned diff --git a/docs/build-windows.md b/docs/build-windows.md index 5a907d4..8653ded 100644 --- a/docs/build-windows.md +++ b/docs/build-windows.md @@ -54,7 +54,8 @@ The builder writes `.offloader-build.json` inside the bundle, an external coverage, and `SHA256SUMS.txt` for the final installer, ZIP, and inventory. A failed build leaves `.offloader-build-incomplete`; it must not be promoted. Source changes during a build invalidate the candidate. These inventories are -provenance and tamper checks, not a complete third-party license inventory or SBOM. +provenance and tamper checks. The third-party licence inventory and the SBOM +are separate outputs of the same build, described below. ## Signing @@ -128,8 +129,7 @@ PEP 440 forms are rejected rather than silently truncated. The spec excludes optional timeline import, disables UPX, and includes the project license and distribution metadata. ffmpeg and ffprobe remain external. -Missing media tools reduce metadata/thumbnails, not copy verification. A -release-ready third-party license inventory and SBOM remain separate work. +Missing media tools reduce metadata/thumbnails, not copy verification. ## Bill of materials and licences @@ -219,6 +219,14 @@ that no job in the workflow attaches what it built to a release. ## Check the artifact +Source validation on 2026-09-21 (Windows x64, Python 3.13.5): 855 tests passed +with 12 skips; lint passed over `src`, `tests`, `scripts` and `build/windows`. +The source archive was built and checked to contain the Windows builder. This +run covers the update client, the tag-triggered candidate workflow and the +bill of materials. **It is a source test run and nothing more:** no bundle was +frozen, nothing was signed, and no installer was executed, so it establishes +none of the gates below. + Installer implementation validation on 2026-09-10 (Windows x64, Python 3.12.10, NSIS 3.12): 727 tests passed with 5 skips and 85% line coverage. Lint passed. The unsigned installer and portable bundle built, and the frozen smoke checks diff --git a/docs/release-plan.md b/docs/release-plan.md index 422fab8..04b3cb5 100644 --- a/docs/release-plan.md +++ b/docs/release-plan.md @@ -112,13 +112,18 @@ credentials, update endpoints, or installation paths. - **Timeline support:** include and test OpenTimelineIO and the currently declared adapter in the desktop bundle if timeline import is advertised for that bundle. Otherwise mark that capability source-only for the beta. -- **Updates:** `offloader update` finds and verifies a release and runs the - signed installer; the in-app check remains deferred. Refuse replacement - while the app or CLI has an active job; never force-kill a copy to install - an update. -- **Scope freeze:** defer new media features, cloud services, notifications, - auto-update, and a marketing website. Fix integrity and packaging blockers - discovered during qualification. +- **Updates:** `offloader update` and the desktop app both find and verify a + release and run the signed installer. Refuse replacement while the app or + CLI has an active job; never force-kill a copy to install an update. See + [updates.md](updates.md). +- **Scope freeze:** defer new media features, cloud services, notifications + and a marketing website. Fix integrity and packaging blockers discovered + during qualification. + + Updating is inside the freeze only as far as it goes today: the *check* is + automatic, and installing never is. Nothing is downloaded, replaced or + restarted without someone asking for it, and a silent background update + stays out of scope. These are working defaults for implementation, not statements that the packaging or safeguards already exist. From 821cff2f6277e24833021aef11de88965e4b2543 Mon Sep 17 00:00:00 2001 From: owenpkent <20529132+owenpkent@users.noreply.github.com> Date: Mon, 21 Sep 2026 21:28:18 -0400 Subject: [PATCH 2/7] Say that the updater cannot see the releases it is documented as ordering This branch exists to make the docs describe the checkout, and one of the claims it added does not. README, the release plan and updates.md all present "offloader update finds a release" as working, and the version ordering section explains at length that prereleases are ordered rather than rejected. The endpoint underneath is /releases/latest, which GitHub documents as returning the newest published full release and excluding prereleases. The first packaged release is planned as 0.1.0b1, so an installed beta cannot discover its successor, and a repository holding only the betas the candidate workflow publishes with --prerelease answers with nothing at all. The ordering is right; the endpoint is wrong. All three places now say so and point at the correction pending on #13, and the release plan no longer counts discovery as a gate that can be signed off. Nothing here claims the fix: it states the limit as the checkout currently has it, which is what this branch is for. --- README.md | 9 +++++++++ docs/release-plan.md | 10 ++++++---- docs/updates.md | 11 +++++++++++ 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 321d8d4..68dd331 100644 --- a/README.md +++ b/README.md @@ -245,6 +245,15 @@ project's certificate thumbprint, its publisher name, and an embedded `FileVersion` matching the release. That last check is what stops an older but still validly signed installer being re-served under a newer asset name. +One limit, stated because this page is meant to describe the checkout rather +than the plan: the feed is GitHub's `/releases/latest`, which is documented as +excluding prereleases. The first packaged release is planned as `0.1.0b1`, so +an installed beta cannot discover the next one through it, and a repository +holding only betas answers with nothing at all. The version grammar already +orders prereleases; it is the endpoint that does not carry them. Reading the +releases collection instead is the correction pending on +[#13](https://github.com/owenpkent/offloader/pull/13). + **It never closes a running Offloader.** The installer refuses maintenance while a transfer is in flight, which is the guarantee rather than a limitation, so updating means finishing or cancelling the job first. The diff --git a/docs/release-plan.md b/docs/release-plan.md index 04b3cb5..73cb4e5 100644 --- a/docs/release-plan.md +++ b/docs/release-plan.md @@ -112,10 +112,12 @@ credentials, update endpoints, or installation paths. - **Timeline support:** include and test OpenTimelineIO and the currently declared adapter in the desktop bundle if timeline import is advertised for that bundle. Otherwise mark that capability source-only for the beta. -- **Updates:** `offloader update` and the desktop app both find and verify a - release and run the signed installer. Refuse replacement while the app or - CLI has an active job; never force-kill a copy to install an update. See - [updates.md](updates.md). +- **Updates:** `offloader update` and the desktop app both check for a + release, verify it, and run the signed installer. Refuse replacement while + the app or CLI has an active job; never force-kill a copy to install an + update. See [updates.md](updates.md). Discovery is not yet a gate that can + be signed off: the feed is `/releases/latest`, which excludes prereleases, + so a beta cannot find its successor until that is corrected. - **Scope freeze:** defer new media features, cloud services, notifications and a marketing website. Fix integrity and packaging blockers discovered during qualification. diff --git a/docs/updates.md b/docs/updates.md index 827c8b6..21ae16c 100644 --- a/docs/updates.md +++ b/docs/updates.md @@ -44,6 +44,17 @@ build that Windows then considers older than the one it replaced. If either version is unreadable the answer is "not newer". String comparison is what makes `1.0.10` look older than `1.0.9`. +**The feed above does not yet carry the prereleases this grammar orders.** +GitHub documents `/releases/latest` as returning the newest published full +release and excluding prereleases, so an installed `0.1.0b1` cannot discover +`0.1.0b2` or `0.1.0rc1` through it, and a repository holding only the betas +the candidate workflow publishes with `--prerelease` answers with nothing at +all. The ordering here is right and the endpoint is wrong; reading the +releases collection instead is the correction pending on +[#13](https://github.com/owenpkent/offloader/pull/13). Until that lands, take +the ordering as describing what the updater would do with a release it can +see, not as evidence that it can see one. + ## What is checked before anything runs | Check | What it stops | From ac7f21a332a35c1a08f8c30f288e87387fb15512 Mon Sep 17 00:00:00 2001 From: Owen Kent Date: Thu, 8 Oct 2026 20:32:56 -0400 Subject: [PATCH 3/7] Describe the update feed and the remaining release work as the combined stack has them --- README.md | 18 +++++++++++------- ROADMAP.md | 15 ++++++++++----- docs/release-plan.md | 7 ++++--- docs/updates.md | 19 ++++++++----------- 4 files changed, 33 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 7206adb..defb551 100644 --- a/README.md +++ b/README.md @@ -251,14 +251,18 @@ project's certificate thumbprint, its publisher name, and an embedded `FileVersion` matching the release. That last check is what stops an older but still validly signed installer being re-served under a newer asset name. +The feed is the releases collection (`/releases?per_page=30`), not +`/releases/latest`, which GitHub documents as excluding prereleases. Drafts are +ignored, the channel is taken from the version in the tag rather than from +GitHub's prerelease flag, and the greatest eligible version wins rather than +whichever is listed first. So an installed beta such as `0.1.0b1` is offered the +next beta, release candidate or final release, while a stable install is +offered stable releases only. + One limit, stated because this page is meant to describe the checkout rather -than the plan: the feed is GitHub's `/releases/latest`, which is documented as -excluding prereleases. The first packaged release is planned as `0.1.0b1`, so -an installed beta cannot discover the next one through it, and a repository -holding only betas answers with nothing at all. The version grammar already -orders prereleases; it is the endpoint that does not carry them. Reading the -releases collection instead is the correction pending on -[#13](https://github.com/owenpkent/offloader/pull/13). +than the plan: the command line reports a feed it could not reach the same way +as one with nothing newer, as "the newest release available". The desktop app +tells the two apart. **It never closes a running Offloader.** The installer refuses maintenance while a transfer is in flight, which is the guarantee rather than a diff --git a/ROADMAP.md b/ROADMAP.md index edabebd..de19f26 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -126,11 +126,16 @@ would close it. offload wants to leave the cart. - **A signed, qualified Windows release.** The packaging is implemented: frozen bundle, NSIS installer, transactional maintenance, signing hooks, a - bill of materials, an update client, and a tag-triggered candidate workflow. - What is left is not code. Signing needs the hardware token, the acceptance - matrix needs a clean machine, and Qt's LGPL/GPL terms against Offloader's - MIT licence need a redistribution decision, which the notices file flags on - every build. See [`docs/release-plan.md`](docs/release-plan.md). + bill of materials for the Python dependency closure, an update client that + reads the releases collection, and a tag-triggered candidate workflow. + Most of what is left is not code. Signing needs the hardware token, the + acceptance matrix needs a clean machine, and Qt's LGPL/GPL terms against + Offloader's MIT licence need a redistribution decision, which the notices + file flags on every build. One part is: the bill of materials does not cover + the CPython runtime or the PyInstaller bootloader, which + `build/windows/sbom.py` names as uncovered, and the release plan keeps a + complete third-party inventory as a gate. See + [`docs/release-plan.md`](docs/release-plan.md). - **Per-job report templates and custom branding.** ## Not planned diff --git a/docs/release-plan.md b/docs/release-plan.md index 77fbd26..da1efde 100644 --- a/docs/release-plan.md +++ b/docs/release-plan.md @@ -124,9 +124,10 @@ credentials, update endpoints, or installation paths. - **Updates:** `offloader update` and the desktop app both check for a release, verify it, and run the signed installer. Refuse replacement while the app or CLI has an active job; never force-kill a copy to install an - update. See [updates.md](updates.md). Discovery is not yet a gate that can - be signed off: the feed is `/releases/latest`, which excludes prereleases, - so a beta cannot find its successor until that is corrected. + update. See [updates.md](updates.md). Discovery reads the releases + collection, ignores drafts and takes the channel from the tag's version, so + a beta can find the next beta. What it still needs is the qualification + below: an end-to-end update between two signed published releases. - **Scope freeze:** defer new media features, cloud services, notifications and a marketing website. Fix integrity and packaging blockers discovered during qualification. diff --git a/docs/updates.md b/docs/updates.md index 5985e5f..aa66b41 100644 --- a/docs/updates.md +++ b/docs/updates.md @@ -71,16 +71,11 @@ build that Windows then considers older than the one it replaced. If either version is unreadable the answer is "not newer". String comparison is what makes `1.0.10` look older than `1.0.9`. -**The feed above does not yet carry the prereleases this grammar orders.** -GitHub documents `/releases/latest` as returning the newest published full -release and excluding prereleases, so an installed `0.1.0b1` cannot discover -`0.1.0b2` or `0.1.0rc1` through it, and a repository holding only the betas -the candidate workflow publishes with `--prerelease` answers with nothing at -all. The ordering here is right and the endpoint is wrong; reading the -releases collection instead is the correction pending on -[#13](https://github.com/owenpkent/offloader/pull/13). Until that lands, take -the ordering as describing what the updater would do with a release it can -see, not as evidence that it can see one. +Because the feed is the releases collection, this ordering is applied to +releases the updater can actually see: an installed `0.1.0b1` finds `0.1.0b2` +or `0.1.0rc1` once either is published, and a repository holding only betas +still answers. The channel comes from the version in the tag, not from GitHub's +`prerelease` flag, which the updater does not read. ## What is checked before anything runs @@ -148,7 +143,9 @@ reports its result to somebody cannot use it: "you are the newest release" is a claim, and it would be made on the strength of a failed DNS lookup. The app uses `check_feed()`, which returns `None` only when the feed answered and had nothing newer, and raises `FeedError` otherwise. A check that could not be -made is reported as a failure. +made is reported as a failure. `offloader update` on the command line still +calls `check()`, so there a failed check prints the same "newest release +available" line as a current one. "Answered" means it returned the releases collection. An empty collection, or one with nothing newer for this install's channel, is up to date. Anything From 909677f5ddcc1887b2bbd5984bcfcb1b3abe92a3 Mon Sep 17 00:00:00 2001 From: owenpkent <20529132+owenpkent@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:29:11 -0600 Subject: [PATCH 4/7] Update test counts --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 49c386b..4970c51 100644 --- a/README.md +++ b/README.md @@ -639,7 +639,7 @@ ruff check src tests pytest --cov=offloader --cov-report=term-missing ``` -1019 tests at 83% line coverage. They cover formatting against the reference's +1023 tests at 83% line coverage. They cover formatting against the reference's exact strings, checksum vectors and streaming equivalence, copy/verify behaviour including simulated destination corruption, pause/resume/cancel concurrency, retry discrimination, BRAW container parsing, ffprobe parsing, From da73d520bfa32418084b5cfce43dec3923d8380f Mon Sep 17 00:00:00 2001 From: owenpkent <20529132+owenpkent@users.noreply.github.com> Date: Sun, 11 Oct 2026 16:54:24 -0600 Subject: [PATCH 5/7] Correct documentation claims to match the current updater, verify, installer and SBOM behaviour --- AGENTS.md | 6 ++++-- CHANGELOG.md | 3 +++ README.md | 32 ++++++++++++++++++++++---------- ROADMAP.md | 11 +++++++---- docs/build-windows.md | 16 ++++++++++------ docs/data-safety.md | 7 +++++-- docs/release-plan.md | 18 +++++++++++------- docs/updates.md | 5 ++++- 8 files changed, 66 insertions(+), 32 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index f2d8532..3dc8e98 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -102,8 +102,10 @@ and verification. Do not make a missing media tool block a valid transfer. writes and unreadable-config fallback. Tests must use temporary state rather than modifying the user's presets or history. - Do not represent known gaps as implemented protections: concurrent instances - are not coordinated, size-only skip is not a checksum check, and directory - structure hashes are not yet rechecked. Consult the current safety document + are not coordinated, size-only skip is not a checksum check, the installer + update's last hash-to-launch window is not closed, and the SBOM does not + cover the bundled CPython, OpenSSL, libffi, VC++/UCRT and bootloader + binaries. Consult the current safety document for the full list before changing claims. ## Development and validation diff --git a/CHANGELOG.md b/CHANGELOG.md index ba00f55..62a243d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,9 @@ project uses [semantic versioning][semver]. ### Fixed +- **Documentation claims were corrected to match the code:** the updater's + re-hash and its remaining window, verify's eviction warning, what an update + closes, the installer's `/D=` handling, and the SBOM's coverage. - **`offloader update` reported a feed it could not read as "up to date".** Being offline, an HTTP error or rate limit, a redirect off the allowlist and malformed JSON all printed "the newest release available" and exited 0, so diff --git a/README.md b/README.md index d277d59..af96796 100644 --- a/README.md +++ b/README.md @@ -255,6 +255,9 @@ destination, including any that failed (recorded with no checksum when none could be taken), so a destination missing a clip fails here; every manifest under the folder is checked, earlier runs' included. `--allow-cache` skips the page-cache eviction: faster, and may verify memory rather than the device. +Without it, a failed eviction is printed as a warning rather than ignored. On +macOS eviction is not available, so the warning is expected there and the +result may reflect memory rather than the device. For an ASC MHL history it also recomputes the directory content and structure hashes, which is the only check that catches a rename or a moved file — every @@ -270,11 +273,17 @@ offloader update --install # download it, verify it, run the installer GitHub Releases is the feed, so there is no manifest server and no second place a version number is recorded. Before anything runs, the download has to -be served from an allowlisted host *after* redirects, hash to what was -computed while streaming, and carry a valid Authenticode signature with this -project's certificate thumbprint, its publisher name, and an embedded -`FileVersion` matching the release. That last check is what stops an older -but still validly signed installer being re-served under a newer asset name. +be served from an allowlisted host *after* redirects, and carry a valid +Authenticode signature with this project's certificate thumbprint, its +publisher name, and an embedded `FileVersion` matching the release. That last +check is what stops an older but still validly signed installer being +re-served under a newer asset name. The SHA-256 taken while streaming is +compared with a fresh hash of the file on disk before the signature is read, +after it is read, and immediately before launch; a mismatch refuses and +nothing runs. One window remains: a process able to write the download +directory between that last hash and the launch could still swap the file, so +point `--dir` only at a private location (see the known limits in +[`docs/updates.md`](docs/updates.md)). The feed is the releases collection (`/releases?per_page=30`), not `/releases/latest`, which GitHub documents as excluding prereleases. Drafts are @@ -289,10 +298,12 @@ available": the command line prints an error and exits with status 2 (0 is up to date, 1 is a newer release found), and the desktop app reports the check as failed. -**It never closes a running Offloader.** The installer refuses maintenance -while a transfer is in flight, which is the guarantee rather than a -limitation, so updating means finishing or cancelling the job first. The -command says so before the elevation prompt appears. +**It never closes a running transfer.** The installer refuses maintenance +while any Offloader program holds the installation, which is the guarantee +rather than a limitation, so updating means finishing or cancelling the job +first. The command line closes nothing and says so before the elevation prompt +appears. The desktop app declines the update while a job is running or paused, +and otherwise closes itself, after asking, once the installer has started. The desktop app does the same thing from **Help, Check for updates now**, and checks once on launch unless that is turned off in Options. Full detail, @@ -577,7 +588,8 @@ The short version: was already there and never leaves a plausible-looking filename. - `--verify full` evicts each file from the page cache before reading it back, because a read straight after a write is otherwise served from memory and - proves nothing about the device. + proves nothing about the device. If eviction fails, which is always the case + on macOS, that is recorded as a warning. - Empty files, and verifications that may have been served from cache, are reported as warnings rather than folded into a "Verified" verdict. - Reads that fail for a transient reason are retried at the failing chunk, so diff --git a/ROADMAP.md b/ROADMAP.md index de19f26..da1bf88 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -131,10 +131,13 @@ would close it. Most of what is left is not code. Signing needs the hardware token, the acceptance matrix needs a clean machine, and Qt's LGPL/GPL terms against Offloader's MIT licence need a redistribution decision, which the notices - file flags on every build. One part is: the bill of materials does not cover - the CPython runtime or the PyInstaller bootloader, which - `build/windows/sbom.py` names as uncovered, and the release plan keeps a - complete third-party inventory as a gate. See + file flags on every build. One part is: the bill of materials covers only the + Python distribution dependencies. The bundle also ships components with no + Python metadata, including the CPython runtime and OpenSSL and libffi DLLs, + the VC++ and UCRT runtimes and the PyInstaller bootloader; + `build/windows/sbom.py` currently reports only the CPython DLL and the + bootloader as uncovered. The release plan keeps a complete third-party + inventory as a gate. See [`docs/release-plan.md`](docs/release-plan.md). - **Per-job report templates and custom branding.** diff --git a/docs/build-windows.md b/docs/build-windows.md index 6ec8d7e..b40ea93 100644 --- a/docs/build-windows.md +++ b/docs/build-windows.md @@ -195,17 +195,21 @@ actually moved. ### What these three files do not cover The Python distribution dependencies, and only those. A frozen application also -ships components that have no packaging metadata for the closure to walk: the -CPython runtime DLL that bundle validation requires, and the PyInstaller -bootloader compiled into each of the three executables. All three outputs say +ships components that have no packaging metadata for the closure to walk, +including the CPython runtime DLL that bundle validation requires, the +PyInstaller bootloader compiled into each of the three executables, CPython's +OpenSSL DLLs and libffi, and the VC++ and UCRT runtime DLLs. All three outputs say so in their own text — a scope line in the notices, a `offloader:scope` property in the SBOM, a comment in the requirements file — because an inventory read as complete while missing the interpreter it ships is worse than one that states its boundary. -`sbom.uncovered_in_bundle()` reports which of those are actually present in a -built bundle, measured against the tree rather than asserted from a list, so -the gap shrinks as it is closed and cannot be closed by editing a constant. +`sbom.uncovered_in_bundle()` reports which of the entries in its `UNCOVERED` +list (currently the CPython DLL and the bootloader executables only) are +present in a built bundle. It is measured against the tree, but it cannot see +a component that list does not name, so the OpenSSL, libffi and VC++/UCRT DLLs +are not reported and the gap is larger than its output. Extend the list before +treating the inventory as complete. The complete third-party inventory remains a gate in [release-plan.md](release-plan.md). diff --git a/docs/data-safety.md b/docs/data-safety.md index b8d2257..51a3843 100644 --- a/docs/data-safety.md +++ b/docs/data-safety.md @@ -130,8 +130,11 @@ checksum finds that. It also reports files present on disk that the manifest does not list, and it evicts each file before reading so a freshly written tree is read off the device, -and warns when the eviction failed, since the verdicts then say nothing about -the device. The unlisted-file scan starts at the root the manifest belongs to (the folder +and warns when the eviction failed (the CLI prints it as `warning:`, and it is +in the report's warnings), since the verdicts then say nothing about the device. +Eviction uses `posix_fadvise` on POSIX systems, which macOS does not provide, so +on macOS it always fails and the warning is expected; it can also fail +elsewhere. `--allow-cache` skips eviction. The unlisted-file scan starts at the root the manifest belongs to (the folder holding `_Reports`), not at the common folder of the files it lists, which for a one-file manifest was the file itself and found nothing. diff --git a/docs/release-plan.md b/docs/release-plan.md index da1efde..893d5b2 100644 --- a/docs/release-plan.md +++ b/docs/release-plan.md @@ -29,11 +29,12 @@ they surface, and that decision has not been made here. Those three files cover the **Python distribution dependencies** and say so in their own text. They are not the complete third-party inventory: a frozen -application also ships the CPython runtime DLL and the PyInstaller bootloader -compiled into each executable, neither of which has packaging metadata for the -closure to walk. `sbom.uncovered_in_bundle` reports which of those are present -in a built bundle, so the gap is measured against the tree rather than -asserted. +application also ships the CPython runtime DLL, the PyInstaller bootloader +compiled into each executable, CPython's OpenSSL and libffi DLLs, and the +VC++ and UCRT runtimes, none of which has packaging metadata for the closure +to walk. `sbom.uncovered_in_bundle` reports only the CPython DLL and the +bootloader executables that are present in a built bundle; the rest are not +yet reported. Hardware-key signing, clean-machine interactive installation and alternate credential checks, the Qt redistribution decision the notices flag, the @@ -233,8 +234,11 @@ The update client is now implemented in `src/offloader/update.py`, wrapped for the desktop app in `src/offloader/gui/updates.py`, and documented in [updates.md](updates.md). It meets the conditions this section set: the signature, publisher and embedded version are all verified before elevation, -and the `/D=` target is computed from the running executable rather than from -the uninstall registry key. +and the `/D=` target is computed from the running executable. The installer +honours an explicit `/D=`, but when it equals the compiled default location it +cannot tell that from no `/D=` and uses the `InstallLocation` registered under +HKLM (writable only by an administrator), so in the common case the registry +value still decides the target; see [updates.md](updates.md). The app never replaces itself under an active transfer. A running or paused job declines the update with a reason, the queue is rechecked immediately before diff --git a/docs/updates.md b/docs/updates.md index ab665da..8272c95 100644 --- a/docs/updates.md +++ b/docs/updates.md @@ -218,7 +218,10 @@ rather than enforced. ## What it deliberately does not do -**It does not close anything.** Offloader's installer refuses maintenance +**It never closes a running transfer.** The command line closes nothing. The +desktop app declines the update while a job is running or paused, and +otherwise closes itself, after asking, once the installer has started. +Offloader's installer refuses maintenance while any Offloader program (the desktop app or a CLI process, busy or idle) holds the installation's shared lock, and exits non-zero without changing the installation; it never force-kills a running copy. That refusal is a promise From 963d0bc9fd9756ab034cbb680024363e3cf4ea92 Mon Sep 17 00:00:00 2001 From: owenpkent <20529132+owenpkent@users.noreply.github.com> Date: Sun, 11 Oct 2026 16:55:11 -0600 Subject: [PATCH 6/7] Update test counts --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index af96796..743a4a0 100644 --- a/README.md +++ b/README.md @@ -676,7 +676,7 @@ ruff check src tests pytest --cov=offloader --cov-report=term-missing ``` -1023 tests at 83% line coverage. They cover formatting against the reference's +1130 tests at 83% line coverage. They cover formatting against the reference's exact strings, checksum vectors and streaming equivalence, copy/verify behaviour including simulated destination corruption, pause/resume/cancel concurrency, retry discrimination, BRAW container parsing, ffprobe parsing, From ca2e751726112831d469ea25fb04d1c4666333de Mon Sep 17 00:00:00 2001 From: owenpkent <20529132+owenpkent@users.noreply.github.com> Date: Sun, 11 Oct 2026 17:28:55 -0600 Subject: [PATCH 7/7] Update test counts --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a76a22e..734f38a 100644 --- a/README.md +++ b/README.md @@ -680,7 +680,7 @@ ruff check src tests pytest --cov=offloader --cov-report=term-missing ``` -1130 tests at 83% line coverage. They cover formatting against the reference's +1152 tests at 83% line coverage. They cover formatting against the reference's exact strings, checksum vectors and streaming equivalence, copy/verify behaviour including simulated destination corruption, pause/resume/cancel concurrency, retry discrimination, BRAW container parsing, ffprobe parsing,