Repository navigation
Add macOS RivetUpdate shared host-side update client - #230
Merged
Merged
Conversation
Every downstream application was hand-rolling the same security-critical Swift flow (signed manifest check, artifact download, verify, atomic swap), and the class of bug that stalled taskly's in-app OTA — AsyncBytes parking its continuation on a lost end-of-stream flag — was free to recur in each copy. This extracts that flow into the platform package as RivetUpdate so hosts configure four facts (feed URL, Ed25519 public key, key id, bundle name) and keep their own UI. - Signed-wrapper verification matches rivet/distribution: Ed25519 over the exact payload bytes before any JSON parsing, schema and key-id checks, closed inner decode. - Delegate-driven downloader: completion is definitive, request/resource timeouts bound silent stalls, task cancellation always resumes promptly. - Signed size enforced twice — a disagreeing Content-Length fails the transfer immediately, and the delivered file must match even without a length; sha256 is chunked over the file on disk. - ditto unpack, bundle-version confirmation, swap-with-rollback script, relaunch and terminate. - Configuration fails closed at construction: HTTPS-only feeds outside loopback drills, raw-32-byte keys, bare *.app bundle names, positive timeouts. 28 tests cover the verification vectors, version comparison, configuration rejections, installed-bundle detection, swap-script content, sha256 known answer, and URLProtocol-stubbed download (exact bytes, monotonic progress, size mismatch, transport failure, cancellation) plus the check() paths.
The same-day taskly verification (turinglambdaai/taskly@27ab8c6) exposed two more coupled defects behind the stuck OTA beyond the AsyncBytes EOF park; both belong in the shared module, not in each product: - The spawned swap script owns the work directory. An unconditional defer cleanup races the script's grace sleep and destroys the payload it is about to unpack; gate the cleanup on a swapLaunched flag and add cleanupStaleWorkDirectories() for hosts to sweep crashed attempts at startup (fresh directories are kept — another process may be updating). - A presented SwiftUI sheet silently vetoes NSApplication.terminate. The module now ends every sheet, terminates, and stops the run loop after a grace delay if the quit was deferred — a surviving old process holding a single-instance lease would otherwise quietly kill the relaunched update. Docs and changelog describe both handoff traps. 29 tests.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
taskly's in-app OTA stalled for weeks on a macOS host-side bug (turinglambdaai/taskly#13): byte-iterating
URLSession.AsyncBytesparked its continuation forever on a lost end-of-stream flag, and the hand-rolled downloader had no watchdog and no escape hatch. The engine side (rivet/distribution, hardened in 0.8.0) is complete — but every product hand-rolls the same ~460 lines of security-critical Swift for the host-native flow the family blessed. That bug class was free to recur in each copy.What
Extract the battle-tested flow into the platform package as
RivetUpdate, so hosts configure four facts (feed URL, Ed25519 public key, key id, bundle name) and keep their own UI:rivet/distribution: Ed25519 over the exact payload bytes before any JSON parsing, schema and key-id checks, closed inner decode.Content-Lengthfails the transfer immediately instead of downloading a wrong artifact to the end; the delivered file must match even without a length. Chunked sha256 over the file on disk.dittounpack, bundle-version confirmation, swap-with-rollback script, relaunch and terminate.*.appbundle names, positive timeouts.Docs: new "Host-side updates on macOS" section in
docs/release-and-updates.mdwith the UX contract (always a Cancel affordance; dismissable except during the final swap) and pointers for Windows (msiexec) / Linux (engine adapter) hosts.Testing
28 new Swift tests (swift-testing), local
swift test --package-path platform/macosfully green alongside the existing suites: verification vectors (wrong key / key id / algorithm / schema / tampered payload / short signature / signed-but-unparsable), version comparison, configuration rejections, installed-bundle detection through the injected seam, swap-script content and quoting, sha256 known answer, URLProtocol-stubbed downloads (exact bytes, monotonic progress, size mismatch, transport failure, cancellation-always-resumes), andcheck()available / up-to-date / bad-status / dev-copy paths. CIswift-runtimejob picks the target up automatically.