Add verified household TV updates and release tooling #60

Merged
justinzeus merged 5 commits from fm/tv-updater-core into main 2026-11-07 21:33:42 +01:00
Owner

Safety review and UI contract delta

Two separate pushed commits:

  • Release-code safety and staged-byte integrity: 383802611cda836a9cac017f65acf609c73bf169.
  • UI controller seams: 51799669e7a47cdb9dc11247884a0ff32d2ffbcb (current head).

The publisher now reads live latest/manifest anonymously before draft creation or resume and immediately before promotion, refusing equal/lower codes. A latest 404 permits a first release only after an empty release list succeeds. The builder fetches main and checks committed HEAD/main ledgers plus durable local consumed-code refs; discarding a ledger append cannot free a consumed code. Direct optimized diagnostics use code 1 and must never be installed on the household box. The PackageInstaller stream is hashed and length-checked against the verified asset before commit; mismatch abandons the session. A manifest-signer-differs regression is included.

UI-facing shape now includes release: StateFlow<UpdateUiState.Available?> (including persisted notes/date/size after verified Ready recreation) and required Failed(kind, message, operation: UpdateOperation) with Check, Download, Install. The fake matches the UI lane's additive initialRelease shape. Old manifest-only Ready records remain readable. Permission and handoff cancellation restores Ready without discarding verified bytes; installer ownership is independent of visible Ready and still blocks duplicate handoffs, including late pending callbacks and success awaiting PackageManager refresh. Install from PermissionNeeded reopens settings.

Current delta verification: release-tooling fixtures 24 passed; focused controller/session-stream cases 29 passed at the UI-seam head. Actual loopback publish CLI, compiled staged-byte integrity and compiled UI-contract smokes passed. The UI smoke proved notes/date/size after recreation, repeated permission settings opening, Ready dismissal with only one owned session, and structured Install failure.

Exact-head gate evidence accepted by firstmate (instruction 006): the single make test invocation at 51799669e7a47cdb9dc11247884a0ff32d2ffbcb completed 976/977 passing, with only SavedPeoplePickerTest.savedPeopleAreVisibleBeforeTheFirstNetworkResponse timing out at line 72. Firstmate identifies this as a known pre-existing main timing flake, fixed by the saved-picker Android-main-queue synchronization change on fm/tv-round2; no repair or rebase belongs in this PR. Firstmate authorized exactly one retry of only org.justinvisser.wolfskin.tv.SavedPeoplePickerTest: 1/1 passed, zero failures/errors/skips, at the exact same head. No test/source edit or full-suite rerun was made. This records the accepted composite evidence, not a claim that the original full invocation was green.

The older full-suite/debug results below belong to c78a8dfa. No new APK build, box action, rebase, real publication or repository creation occurred. Waiting for firstmate's Opus delta statuses and Justin's go for main; this worker does not merge.


Outcome

AI-assisted (fork-internal). Add the verified household update core and release tooling for org.justinvisser.wolfskin.tv.opt; no distribution repository or release is created by this change.

Shared UI interface

The early interface commit (152dac97) defines UpdateController, UpdateUiState, UpdateFailure, InstalledVersion and FakeUpdateController in org.justinvisser.wolfskin.tv.update. The controller exposes state, installed identity, persisted auto-check preference, check/download/cancel/install/retry and resume handling. The separate Settings UI can bind to that interface. The classic Opt updater delegates to the same service rather than retaining an unsafe second download path.

Applied decisions

  • Keep the Opt package, persistent household debug signer, and existing R8/non-debuggable/baseline-profile defaultOptimized assembly. The stable name is 1.1.1; an explicit monotonic code is mandatory. The proposed first code 100001 exceeds the supplied installed inventory (Opt 65, production 28, Wholphin 62). The tracked ledger starts with used code 65; a dry-run does not reserve a new code.
  • Use the dedicated public Forgejo release API for justinzeus/wolfskin-tv-updates. Migrate only the known old default for Opt, preserve custom URLs, and read installed-release notes from the same channel. The old org.justinvisser.wolfskin.tv package keeps its original repository, parser and legacy installer path; its behavior tests remain.
  • Read schema-1 metadata; prefer device-supported armeabi-v7a, then other supported ABIs. Universal fallback does not make an unsupported processor compatible. Dedicated anonymous HTTPS transport validates each redirect and never borrows the Jellyfin client, tokens, cookies or authentication. Loopback HTTP requires the explicit test flag and a debuggable build.
  • Download one asset privately, report progress, remove partials on cancel, and restart partial retries safely. Require exact length/hash, package, current installed signer plus manifest signer, manifest identity and a strictly higher Android code before Ready. Reverify persisted Ready after recreation; never install at download EOF.
  • Gate unknown-app permission for the exact package, recheck on resume, and hand off through a user-confirmed PackageInstaller session. Handle pending/success/failure/cancel/no-handler outcomes; retain the verified artifact after cancellation and refresh identity from PackageManager. Session outcomes survive controller/process recreation without leaving a false HandingOff state.
  • make tv-release VERSION=1.1.1 VERSION_CODE=<n> requires a clean commit and the existing key, refuses used/lower codes, inspects final signed APKs, and writes four standalone APKs, actual hashes, manifest and notes. make tv-publish is separate: draft upload, byte verification, explicit --promote, anonymous post-promotion verification and draft withdrawal on verification failure. Credential-helper output stays in process memory with tracing disabled. Document signer authority, source offers, ledger reservation and keep-data policy in docker/README.md.
  • Resolve the pinned Android 9/11/14 Robolectric SDKs independently. The initial full gate exposed Gradle evicting the two older versions from one configuration, causing 109 tests to fail before their bodies; this fixes the prerequisite without changing test SDKs or disabling offline execution.
  • Remove the two source-regex/wording checks in OwnedSourceAuditTest; keep behavioral/rendered capture and token-drift coverage. Also replace the adapter's raw spacing with its existing design token.

Verification

Final head: c78a8dfa8dc8b9b6ce89673cee8f0116e3c78a78 (pushed).

Check Cases Failures / errors / skips
App JVM suite 741 0 / 0 / 0
Design JVM/capture suite 202 0 / 0 / 0
Conductarr client suite 26 0 / 0 / 0
Final-head make test total 969 0 / 0 / 0
Separate release tooling / fake Forgejo 15 0 / 0 / 0

The app total includes 49 updater-related cases: controller transitions and HTTP/error/credential/persistence boundaries, manifest/ABI checks, code ordering and legacy channel/parser behavior.

  • One final-head make apk-debug passed. Four debug APKs exist for package org.justinvisser.wolfskin.tv.debug; these use the MPV stub and are not distribution builds. apksigner verify --print-certs on the actual universal debug APK passed and matched the required household certificate SHA-256 182bac72e292b9ea4d3d4e6383f92eb2161fa28ba5afcff0337cb7d5a9d5341d.
  • Clean-source make tv-release VERSION=1.1.1 VERSION_CODE=100001 ... --dry-run passed at this head, without building, tagging, reserving a code or publishing.
  • Real local HTTP plus compiled production-controller smoke proved manual check with auto-check off, verified Ready, recreation, no install at EOF, retained cancellation and no re-download. Android package/installer identity is a fixture boundary in these JVM checks.
  • Failing-before/passing-after runtime probes cover replacement-process install return, interrupted first metadata fetch, re-entry during pending confirmation, and unsupported-processor universal fallback. Permanent behavior regressions cover them.
  • One external native Compose/Robolectric smoke passed: TV focus + D-pad Center opens long notes, Down reaches the final paragraph, system Back dismisses the dialog, and Ready exposes explicit Install. Four PNGs were inspected. This uses the real classic adapter and a fake controller, not the new Settings UI or a physical TV.
  • Ruff check/format and bash -n passed for the tooling. Temporary smoke scaffolds were removed; proof remains outside the checkout.

Not in this change

  • The approved new Settings Updates screens; only the minimal classic bridge is included.
  • Creating justinzeus/wolfskin-tv-updates, any real draft/upload/publication, or a real optimized release build.
  • ADB/device/box actions, actual OEM permission/installer interaction, real-device PackageManager signature execution, or the retained-settings/login in-place TV proof. Those are the later build/real-box walk, not claims of these JVM and fixture checks.

No product decisions remain for Justin in this core change. Installer/signature and cross-family reviews are arranged by firstmate; this pull request does not merge itself.

## Safety review and UI contract delta Two separate pushed commits: - Release-code safety and staged-byte integrity: `383802611cda836a9cac017f65acf609c73bf169`. - UI controller seams: `51799669e7a47cdb9dc11247884a0ff32d2ffbcb` (current head). The publisher now reads live latest/manifest anonymously before draft creation or resume and immediately before promotion, refusing equal/lower codes. A latest 404 permits a first release only after an empty release list succeeds. The builder fetches main and checks committed HEAD/main ledgers plus durable local consumed-code refs; discarding a ledger append cannot free a consumed code. Direct optimized diagnostics use code 1 and must never be installed on the household box. The PackageInstaller stream is hashed and length-checked against the verified asset before commit; mismatch abandons the session. A manifest-signer-differs regression is included. UI-facing shape now includes `release: StateFlow<UpdateUiState.Available?>` (including persisted notes/date/size after verified Ready recreation) and required `Failed(kind, message, operation: UpdateOperation)` with `Check`, `Download`, `Install`. The fake matches the UI lane's additive `initialRelease` shape. Old manifest-only Ready records remain readable. Permission and handoff cancellation restores Ready without discarding verified bytes; installer ownership is independent of visible Ready and still blocks duplicate handoffs, including late pending callbacks and success awaiting PackageManager refresh. Install from PermissionNeeded reopens settings. **Current delta verification:** release-tooling fixtures **24 passed**; focused controller/session-stream cases **29 passed** at the UI-seam head. Actual loopback publish CLI, compiled staged-byte integrity and compiled UI-contract smokes passed. The UI smoke proved notes/date/size after recreation, repeated permission settings opening, Ready dismissal with only one owned session, and structured Install failure. **Exact-head gate evidence accepted by firstmate (instruction 006):** the single `make test` invocation at `51799669e7a47cdb9dc11247884a0ff32d2ffbcb` completed **976/977 passing**, with only `SavedPeoplePickerTest.savedPeopleAreVisibleBeforeTheFirstNetworkResponse` timing out at line 72. Firstmate identifies this as a known pre-existing main timing flake, fixed by the saved-picker Android-main-queue synchronization change on `fm/tv-round2`; no repair or rebase belongs in this PR. Firstmate authorized exactly one retry of **only** `org.justinvisser.wolfskin.tv.SavedPeoplePickerTest`: **1/1 passed**, zero failures/errors/skips, at the exact same head. No test/source edit or full-suite rerun was made. This records the accepted composite evidence, not a claim that the original full invocation was green. The older full-suite/debug results below belong to c78a8dfa. No new APK build, box action, rebase, real publication or repository creation occurred. Waiting for firstmate's Opus delta statuses and Justin's go for main; this worker does not merge. --- ## Outcome AI-assisted (fork-internal). Add the verified household update core and release tooling for `org.justinvisser.wolfskin.tv.opt`; no distribution repository or release is created by this change. ## Shared UI interface The early interface commit (`152dac97`) defines `UpdateController`, `UpdateUiState`, `UpdateFailure`, `InstalledVersion` and `FakeUpdateController` in `org.justinvisser.wolfskin.tv.update`. The controller exposes state, installed identity, persisted auto-check preference, check/download/cancel/install/retry and resume handling. The separate Settings UI can bind to that interface. The classic Opt updater delegates to the same service rather than retaining an unsafe second download path. ## Applied decisions - Keep the Opt package, persistent household debug signer, and existing R8/non-debuggable/baseline-profile `defaultOptimized` assembly. The stable name is `1.1.1`; an explicit monotonic code is mandatory. The proposed first code `100001` exceeds the supplied installed inventory (Opt 65, production 28, Wholphin 62). The tracked ledger starts with used code 65; a dry-run does not reserve a new code. - Use the dedicated public Forgejo release API for `justinzeus/wolfskin-tv-updates`. Migrate only the known old default for Opt, preserve custom URLs, and read installed-release notes from the same channel. The old `org.justinvisser.wolfskin.tv` package keeps its original repository, parser and legacy installer path; its behavior tests remain. - Read schema-1 metadata; prefer device-supported armeabi-v7a, then other supported ABIs. Universal fallback does not make an unsupported processor compatible. Dedicated anonymous HTTPS transport validates each redirect and never borrows the Jellyfin client, tokens, cookies or authentication. Loopback HTTP requires the explicit test flag and a debuggable build. - Download one asset privately, report progress, remove partials on cancel, and restart partial retries safely. Require exact length/hash, package, current installed signer plus manifest signer, manifest identity and a strictly higher Android code before Ready. Reverify persisted Ready after recreation; never install at download EOF. - Gate unknown-app permission for the exact package, recheck on resume, and hand off through a user-confirmed PackageInstaller session. Handle pending/success/failure/cancel/no-handler outcomes; retain the verified artifact after cancellation and refresh identity from PackageManager. Session outcomes survive controller/process recreation without leaving a false HandingOff state. - `make tv-release VERSION=1.1.1 VERSION_CODE=<n>` requires a clean commit and the existing key, refuses used/lower codes, inspects final signed APKs, and writes four standalone APKs, actual hashes, manifest and notes. `make tv-publish` is separate: draft upload, byte verification, explicit `--promote`, anonymous post-promotion verification and draft withdrawal on verification failure. Credential-helper output stays in process memory with tracing disabled. Document signer authority, source offers, ledger reservation and keep-data policy in `docker/README.md`. - Resolve the pinned Android 9/11/14 Robolectric SDKs independently. The initial full gate exposed Gradle evicting the two older versions from one configuration, causing 109 tests to fail before their bodies; this fixes the prerequisite without changing test SDKs or disabling offline execution. - Remove the two source-regex/wording checks in `OwnedSourceAuditTest`; keep behavioral/rendered capture and token-drift coverage. Also replace the adapter's raw spacing with its existing design token. ## Verification Final head: `c78a8dfa8dc8b9b6ce89673cee8f0116e3c78a78` (pushed). | Check | Cases | Failures / errors / skips | | --- | ---: | --- | | App JVM suite | 741 | 0 / 0 / 0 | | Design JVM/capture suite | 202 | 0 / 0 / 0 | | Conductarr client suite | 26 | 0 / 0 / 0 | | **Final-head `make test` total** | **969** | **0 / 0 / 0** | | Separate release tooling / fake Forgejo | 15 | 0 / 0 / 0 | The app total includes 49 updater-related cases: controller transitions and HTTP/error/credential/persistence boundaries, manifest/ABI checks, code ordering and legacy channel/parser behavior. - One final-head `make apk-debug` passed. Four debug APKs exist for package `org.justinvisser.wolfskin.tv.debug`; these use the MPV stub and are not distribution builds. `apksigner verify --print-certs` on the actual universal debug APK passed and matched the required household certificate SHA-256 `182bac72e292b9ea4d3d4e6383f92eb2161fa28ba5afcff0337cb7d5a9d5341d`. - Clean-source `make tv-release VERSION=1.1.1 VERSION_CODE=100001 ... --dry-run` passed at this head, without building, tagging, reserving a code or publishing. - Real local HTTP plus compiled production-controller smoke proved manual check with auto-check off, verified Ready, recreation, no install at EOF, retained cancellation and no re-download. Android package/installer identity is a fixture boundary in these JVM checks. - Failing-before/passing-after runtime probes cover replacement-process install return, interrupted first metadata fetch, re-entry during pending confirmation, and unsupported-processor universal fallback. Permanent behavior regressions cover them. - One external native Compose/Robolectric smoke passed: TV focus + D-pad Center opens long notes, Down reaches the final paragraph, system Back dismisses the dialog, and Ready exposes explicit Install. Four PNGs were inspected. This uses the real classic adapter and a fake controller, not the new Settings UI or a physical TV. - Ruff check/format and `bash -n` passed for the tooling. Temporary smoke scaffolds were removed; proof remains outside the checkout. ## Not in this change - The approved new Settings Updates screens; only the minimal classic bridge is included. - Creating `justinzeus/wolfskin-tv-updates`, any real draft/upload/publication, or a real optimized release build. - ADB/device/box actions, actual OEM permission/installer interaction, real-device PackageManager signature execution, or the retained-settings/login in-place TV proof. Those are the later build/real-box walk, not claims of these JVM and fixture checks. No product decisions remain for Justin in this core change. Installer/signature and cross-family reviews are arranged by firstmate; this pull request does not merge itself.
justinzeus added 3 commits 2026-11-07 19:41:07 +01:00
Author
Owner

Safety review (Tier C, Opus): BLOCKING, 1 finding

Reviewed head: c78a8dfa8dc8b9b6ce89673cee8f0116e3c78a78. Status review/safety-opus = failure on this SHA. I found no path that installs an unverified, wrong-signer, wrong-package, equal-code or lower-code APK, deletes or resets data, forces an uninstall, leaks a credential, or publishes without --promote. The one blocker is in the release tooling: a used or lower versionCode can reach the channel.

Blocking

B1. A used or lower versionCode can be released and published. tv_release.py validate_inputs takes max() from the working-tree docker/tv-version-codes, and tv_publish.py has no monotonic check. Four ways through:

  1. Any clean stale branch or commit whose committed ledger predates a reservation passes.
  2. git checkout -- docker/tv-version-codes after a release resets the reservation.
  3. Direct optimized Gradle builds skip the ledger. The README's direct-build example uses -PtvVersionCode=100001, the proposed first release code; a box bootstrapped with it would treat the real release as "Up to date".
  4. tv_publish.py publishes any code, including one at or below the current latest.

Probe with the real validate_inputs: stale branch -> ACCEPTED, after discarding ledger append -> ACCEPTED, while main with a committed 100001 is refused. Devices refuse equal or lower codes (client and Android), so the impact is a stuck or split channel, not data loss.
Fix: (a) before draft creation and again before --promote, tv_publish.py fetches the anonymous latest wolfskin-update.json and refuses unless bundle code > latest code (404 with an empty list = first release); (b) tv_release.py also takes the max from origin/main:docker/tv-version-codes; (c) change the README direct-build example away from a real release code.

Non-blocking

  1. Verify-to-session TOCTOU (low): install() verifies the file, then AndroidUpdatePlatform.install re-reads it. The file is app-private, operations are serialized, and Android enforces signer, package and no-downgrade. Hardening: hash while streaming into the session and abandon on mismatch.
  2. UpdateChannel.SIGNER is unused in production. Trust is archive == installed == manifest signer, which is correct; a box with an older signer gets WrongSigner, not an install. Pin explicitly or delete the constant.
  3. No origin pinning for browser_download_url or redirect targets (any HTTPS). Acceptable given hash plus signer continuity.
  4. Test gaps: no production-config https→http redirect test; no manifest-signer≠installed test at check(); compatibleArm32IsPreferredAndUniversalRespectsMinimumSdk fails on ABI, not minSdk. The app allows cleartext app-wide, so validateUrl is the only HTTPS gate; it is correct.
  5. OwnedSourceAuditTest deleted (both tests): style, token, font-floor and household-label wording rules. Nothing safety-relevant lost; unrelated to the updater.
  6. versionName is not monotonic or floored, and AppUpgradeHandler.kt:456-477 clears current server and user when current <= 1.0.8. Require VERSION ≥ 1.1.1 and ≥ the last ledger name.
  7. tv-release creates the local, unpushed tag pwolfskin-opt-v<VERSION> as part of the explicit command. tv_publish should verify that the remote source tag exists and points at sourceCommit before --promote (otherwise the GPL source link can 404).
  8. No post-build cleanliness re-check; ignored files are not covered.
  9. Resumed drafts: extra or duplicate assets are not refused before --promote.
  10. Credentials OK: tracing stripped, same-origin Authorization only, no redirects for POST/PATCH, token only in memory and in container env; the app transport has no interceptors or cookies.
  11. Robolectric: safe fix of version-conflict eviction. Same coordinates; offline and dependency-dir unchanged.
  12. Conflicts: the round 2 PRs #58 and #59 merge clean (only docker/README.md is shared, with #59). fm/tv-updater-ui sits on 152dac97 only. It adds UpdateController.release and changes cancel() semantics, which this head's HouseholdUpdateController does not implement, so the rebase needs controller (core) changes and a delta safety check.
  13. Pre-existing README text recommends adb uninstall -k "if unavoidable". Outside this diff; FYI.
  14. The household signing key (a debug-format keystore under the firstmate data dir) is the root of trust; escrow it with the tank secrets.
  15. Cosmetic: a cancel racing install() can still reach the user-confirmed session.

Confirmed safe:

  • No install at download end.
  • User-confirmed PackageInstaller session (USER_ACTION_REQUIRED) with a non-exported receiver checked against the session id.
  • Per-package unknown-app permission, with another Install press required after settings.
  • Length, SHA-256, package, signer and code checks before Ready; re-verified on restore and at install.
  • Strict code ordering; no -d, uninstall or pm clear anywhere.
  • HTTPS validated on every hop; loopback only with DEBUG plus the test flag.
  • armeabi-v7a preference; universal never makes an unsupported processor compatible.
  • Legacy package path unchanged.
  • Release builds inspected with aapt2/apksigner; key never generated.
  • Draft → verify → explicit --promote → anonymous verify → withdraw on failure.
  • Nothing creates the distribution repository.
## Safety review (Tier C, Opus): BLOCKING, 1 finding Reviewed head: `c78a8dfa8dc8b9b6ce89673cee8f0116e3c78a78`. Status `review/safety-opus` = failure on this SHA. I found no path that installs an unverified, wrong-signer, wrong-package, equal-code or lower-code APK, deletes or resets data, forces an uninstall, leaks a credential, or publishes without `--promote`. The one blocker is in the release tooling: a used or lower versionCode can reach the channel. ### Blocking **B1. A used or lower versionCode can be released and published.** `tv_release.py` `validate_inputs` takes `max()` from the working-tree `docker/tv-version-codes`, and `tv_publish.py` has no monotonic check. Four ways through: 1. Any clean stale branch or commit whose committed ledger predates a reservation passes. 2. `git checkout -- docker/tv-version-codes` after a release resets the reservation. 3. Direct optimized Gradle builds skip the ledger. The README's direct-build example uses `-PtvVersionCode=100001`, the proposed first release code; a box bootstrapped with it would treat the real release as "Up to date". 4. `tv_publish.py` publishes any code, including one at or below the current latest. Probe with the real `validate_inputs`: `stale branch -> ACCEPTED`, `after discarding ledger append -> ACCEPTED`, while main with a committed 100001 is refused. Devices refuse equal or lower codes (client and Android), so the impact is a stuck or split channel, not data loss. Fix: (a) before draft creation and again before `--promote`, `tv_publish.py` fetches the anonymous latest `wolfskin-update.json` and refuses unless bundle code > latest code (404 with an empty list = first release); (b) `tv_release.py` also takes the max from `origin/main:docker/tv-version-codes`; (c) change the README direct-build example away from a real release code. ### Non-blocking 1. Verify-to-session TOCTOU (low): `install()` verifies the file, then `AndroidUpdatePlatform.install` re-reads it. The file is app-private, operations are serialized, and Android enforces signer, package and no-downgrade. Hardening: hash while streaming into the session and abandon on mismatch. 2. `UpdateChannel.SIGNER` is unused in production. Trust is archive == installed == manifest signer, which is correct; a box with an older signer gets WrongSigner, not an install. Pin explicitly or delete the constant. 3. No origin pinning for `browser_download_url` or redirect targets (any HTTPS). Acceptable given hash plus signer continuity. 4. Test gaps: no production-config https→http redirect test; no manifest-signer≠installed test at `check()`; `compatibleArm32IsPreferredAndUniversalRespectsMinimumSdk` fails on ABI, not minSdk. The app allows cleartext app-wide, so `validateUrl` is the only HTTPS gate; it is correct. 5. `OwnedSourceAuditTest` deleted (both tests): style, token, font-floor and household-label wording rules. Nothing safety-relevant lost; unrelated to the updater. 6. versionName is not monotonic or floored, and `AppUpgradeHandler.kt:456-477` clears current server and user when `current <= 1.0.8`. Require VERSION ≥ 1.1.1 and ≥ the last ledger name. 7. `tv-release` creates the local, unpushed tag `pwolfskin-opt-v<VERSION>` as part of the explicit command. `tv_publish` should verify that the remote source tag exists and points at `sourceCommit` before `--promote` (otherwise the GPL source link can 404). 8. No post-build cleanliness re-check; ignored files are not covered. 9. Resumed drafts: extra or duplicate assets are not refused before `--promote`. 10. Credentials OK: tracing stripped, same-origin Authorization only, no redirects for POST/PATCH, token only in memory and in container env; the app transport has no interceptors or cookies. 11. Robolectric: safe fix of version-conflict eviction. Same coordinates; offline and dependency-dir unchanged. 12. Conflicts: the round 2 PRs https://git.justinvisser.org/justinzeus/wolfskin/pulls/58 and https://git.justinvisser.org/justinzeus/wolfskin/pulls/59 merge clean (only `docker/README.md` is shared, with #59). **`fm/tv-updater-ui` sits on `152dac97` only.** It adds `UpdateController.release` and changes `cancel()` semantics, which this head's `HouseholdUpdateController` does not implement, so the rebase needs controller (core) changes and a delta safety check. 13. Pre-existing README text recommends `adb uninstall -k` "if unavoidable". Outside this diff; FYI. 14. The household signing key (a debug-format keystore under the firstmate data dir) is the root of trust; escrow it with the tank secrets. 15. Cosmetic: a cancel racing `install()` can still reach the user-confirmed session. Confirmed safe: - No install at download end. - User-confirmed PackageInstaller session (USER_ACTION_REQUIRED) with a non-exported receiver checked against the session id. - Per-package unknown-app permission, with another Install press required after settings. - Length, SHA-256, package, signer and code checks before Ready; re-verified on restore and at install. - Strict code ordering; no `-d`, uninstall or `pm clear` anywhere. - HTTPS validated on every hop; loopback only with DEBUG plus the test flag. - armeabi-v7a preference; universal never makes an unsupported processor compatible. - Legacy package path unchanged. - Release builds inspected with aapt2/apksigner; key never generated. - Draft → verify → explicit `--promote` → anonymous verify → withdraw on failure. - Nothing creates the distribution repository.
justinzeus added 1 commit 2026-11-07 20:19:04 +01:00
justinzeus added 1 commit 2026-11-07 20:29:59 +01:00
Expose persistent update metadata and safe UI dismissals
All checks were successful
review/safety-opus Delta PASS: B1 closed; seams keep install invariants
51799669e7
Author
Owner

Safety review delta (Opus): PASS. B1 closed

Reviewed head: 51799669e7a47cdb9dc11247884a0ff32d2ffbcb. Scope: git diff c78a8dfa..51799669 only (the monotonic-release and staged-integrity fix 38380261, plus the UI contract seams 51799669). Status review/safety-opus = success on this SHA.

B1 (reused or lower versionCode): closed on all four paths

  • Stale branch: validate_inputs fetches origin/main (and refuses if the fetch fails) and takes the max across the HEAD ledger, the origin/main ledger and local reservation refs. Re-running my throwaway probe against the new code: a stale branch with a code reserved on main is refused.
  • Discarded reservation: before the build, a create-only git update-ref refs/wolfskin-tv/version-codes/<code> reserves the code. Probe: discarding the ledger append is refused, also from a stale branch, and reserving the same code twice is refused.
  • Direct builds: the README example now uses -PtvVersionCode=1, and direct optimized builds are documented as never installed or published.
  • Publish gate: require_newer_code reads latest and its manifest anonymously, before any credential or draft and again immediately before --promote. The bundle code must be strictly greater. A 404 counts as a first release only when the release list is empty; anything else fails closed. Tooling tests run with plain python3 -m unittest: 24/24 OK, including equal/lower refused, channel advance blocks promotion, and non-empty 404 fails closed.

Earlier note 1 (verify-to-session TOCTOU): closed. copyVerifiedUpdate hashes and counts the exact bytes streamed into the session and refuses before commit; the session is abandoned and Ready cleared.

Seams keep the invariants

  • Duplicate handoff is still refused, and more strictly. installerActive is set before platform.install and folded into systemOwnsFlow. install(), check() and download() all refuse while the system installer owns the session.
  • Cancel from PermissionNeeded or HandingOff deletes only download.part, keeps verified.apk and ready.json, restores Ready, and never installs.
  • The persisted releaseNotes is an optional extra field; old manifest-only records still parse (ignoreUnknownKeys, tested). restore re-hashes and re-inspects before any Ready, so notes cannot make an unverified artifact Ready.
  • Unchanged: no install at download end, strict higher code, and verification of the exact bytes handed to the session.

Non-blocking

  • D1. Cancel from HandingOff does not abandonSession, so the system confirmation can still appear; the user must still confirm. If no outcome ever arrives, installerActive stays latched for the process lifetime: Install, Check and Download silently do nothing while the screen shows Ready. Liveness only.
  • D2. Reservation refs are per clone. Another clone relies on the origin/main ledger plus the live-channel gate.
  • D3. Earlier notes 6 (versionName floor; AppUpgradeHandler resets when current <= 1.0.8), 7 (remote source-tag check) and 9 (exact asset set on resumed drafts) remain open, non-blocking.
## Safety review delta (Opus): PASS. B1 closed Reviewed head: `51799669e7a47cdb9dc11247884a0ff32d2ffbcb`. Scope: `git diff c78a8dfa..51799669` only (the monotonic-release and staged-integrity fix `38380261`, plus the UI contract seams `51799669`). Status `review/safety-opus` = success on this SHA. **B1 (reused or lower versionCode): closed on all four paths** - **Stale branch:** `validate_inputs` fetches `origin/main` (and refuses if the fetch fails) and takes the max across the HEAD ledger, the `origin/main` ledger and local reservation refs. Re-running my throwaway probe against the new code: a stale branch with a code reserved on main is refused. - **Discarded reservation:** before the build, a create-only `git update-ref refs/wolfskin-tv/version-codes/<code>` reserves the code. Probe: discarding the ledger append is refused, also from a stale branch, and reserving the same code twice is refused. - **Direct builds:** the README example now uses `-PtvVersionCode=1`, and direct optimized builds are documented as never installed or published. - **Publish gate:** `require_newer_code` reads latest and its manifest anonymously, before any credential or draft and again immediately before `--promote`. The bundle code must be strictly greater. A 404 counts as a first release only when the release list is empty; anything else fails closed. Tooling tests run with plain `python3 -m unittest`: 24/24 OK, including equal/lower refused, channel advance blocks promotion, and non-empty 404 fails closed. **Earlier note 1 (verify-to-session TOCTOU): closed.** `copyVerifiedUpdate` hashes and counts the exact bytes streamed into the session and refuses before commit; the session is abandoned and Ready cleared. **Seams keep the invariants** - **Duplicate handoff is still refused, and more strictly.** `installerActive` is set before `platform.install` and folded into `systemOwnsFlow`. `install()`, `check()` and `download()` all refuse while the system installer owns the session. - **Cancel from PermissionNeeded or HandingOff** deletes only `download.part`, keeps `verified.apk` and `ready.json`, restores Ready, and never installs. - **The persisted `releaseNotes`** is an optional extra field; old manifest-only records still parse (`ignoreUnknownKeys`, tested). `restore` re-hashes and re-inspects before any Ready, so notes cannot make an unverified artifact Ready. - **Unchanged:** no install at download end, strict higher code, and verification of the exact bytes handed to the session. **Non-blocking** - D1. Cancel from HandingOff does not `abandonSession`, so the system confirmation can still appear; the user must still confirm. If no outcome ever arrives, `installerActive` stays latched for the process lifetime: Install, Check and Download silently do nothing while the screen shows Ready. Liveness only. - D2. Reservation refs are per clone. Another clone relies on the `origin/main` ledger plus the live-channel gate. - D3. Earlier notes 6 (versionName floor; `AppUpgradeHandler` resets when `current <= 1.0.8`), 7 (remote source-tag check) and 9 (exact asset set on resumed drafts) remain open, non-blocking.
justinzeus merged commit 57cf17aaad into main 2026-11-07 21:33:42 +01:00
Sign in to join this conversation.
No reviewers
No labels
exploring
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: justinzeus/wolfskin#60
No description provided.