On this page:
update-artifact
update-manifest
updater-config
update-candidate
install-plan
updater-settings-adapter
updater-runtime
make-updater-runtime
updater-runtime-state
updater-runtime-check!
updater-runtime-download!
silent-check-due?
mark-silent-checked!
draw-rollout-bucket
resolve-rollout-bucket!
platform-installation?
8.1 Versions and Channels
version?
version-compare
version<?
version<=?
version=?
version>=?
version>?
valid-channel?
channel-accepts-version?
8.2 Release Target Metadata
platform-symbol
architecture-symbol
installer-extension
rfc3339-now
8.3 Signed Manifests
write-signed-manifest
read-signed-manifest
verify-signed-manifest
manifest-schema-version
manifest->payload-bytes
payload-bytes->manifest
8.4 Key and Digest Primitives
read-ed25519-public-key
read-ed25519-private-key
bytes->ed25519-public-key
ed25519-sign
ed25519-verify
sha256-bytes
sha256-file
sha256-file/  hex
bytes->base64-string
base64-string->bytes
8.5 Selecting and Installing an Update
fetch-update-manifest
fetch-sibling-asset
select-update
download-update
verify-update-artifact!
make-install-plan
8.6 Managed Check and Download Lifecycle
current-update-architecture
make-updater
updater?
updater-check!
updater-download!
updater-cancel!
updater-state
updater-wait
execute-install-plan!
recover-install-plan!
8.7 First-Party Platform Replacement
current-update-platform
current-install-kind
platform-installer-policy
verify-platform-payload!
prepare-platform-installation
execute-platform-installation!
recover-platform-installation!

8 Distribution and Secure Updates🔗ℹ

 (require rivet/distribution) package: rivet

The updater treats HTTPS as transport protection, not as its root of trust. Manifests are independently signed with Ed25519 and bind the application ID, version, channel, rollout, rollback policy, artifact byte size, SHA-256 digest, platform, and architecture.

struct

(struct update-artifact (platform
    architecture
    url
    sha256
    size
    installer
    arguments
    component))
  platform : symbol?
  architecture : symbol?
  url : string?
  sha256 : string?
  size : exact-nonnegative-integer?
  installer : symbol?
  arguments : (listof string?)
  component : (or/c #f symbol?)
Describes one installer artifact bound into the signed manifest. The component discriminates several installables built from one release ('app, 'cli, or 'tool); #f keeps the legacy single-artifact shape.

struct

(struct update-manifest (application-id
    version
    build
    channel
    published-at
    minimum-version
    previous-version
    rollback-allowed?
    rollout
    artifacts))
  application-id : string?
  version : string?
  build : exact-positive-integer?
  channel : symbol?
  published-at : string?
  minimum-version : string?
  previous-version : (or/c #f string?)
  rollback-allowed? : boolean?
  rollout : exact-nonnegative-integer?
  artifacts : (listof update-artifact?)
Represents the parsed payload covered by an Ed25519 signature.

struct

(struct updater-config (application-id
    current-version
    channel
    platform
    architecture
    public-key
    expected-key-id
    rollout-bucket
    maximum-download-bytes
    component))
  application-id : string?
  current-version : string?
  channel : symbol?
  platform : symbol?
  architecture : symbol?
  public-key : any/c
  expected-key-id : (or/c #f string?)
  rollout-bucket : exact-nonnegative-integer?
  maximum-download-bytes : exact-positive-integer?
  component : (or/c #f symbol?)
Holds local identity, trust, rollout, and resource-limit policy. The component selects between several installables on the same platform; a config without one matches component-less artifacts only.

struct

(struct update-candidate (manifest artifact))

  manifest : update-manifest?
  artifact : update-artifact?
Pairs an accepted manifest with its current-platform artifact.

struct

(struct install-plan (candidate
    downloaded-path
    backup-path
    install
    restart
    rollback))
  candidate : update-candidate?
  downloaded-path : path-string?
  backup-path : (or/c #f path-string?)
  install : procedure?
  restart : procedure?
  rollback : procedure?
Contains the verified artifact and platform-owned lifecycle callbacks.

struct

(struct updater-settings-adapter (get set!))

  get : procedure?
  set! : procedure?
Bridges the updater scaffolding to the product’s settings store: get answers a string key with a string or #f, set! persists one.

struct

(struct updater-runtime (config
    destination-dir
    settings
    fetch
    download
    box
    silent-interval))
  config : updater-config?
  destination-dir : path-string?
  settings : updater-settings-adapter?
  fetch : procedure?
  download : procedure?
  box : box?
  silent-interval : real?
The product’s update surface: config, destination, settings bridge, injectable transport hooks, the phase box, and the silent-check interval. Run updater-runtime-check! and updater-runtime-download! on a product-owned worker thread; the RVT1 layer observes updater-runtime-state.

procedure

(make-updater-runtime 
  #:config config 
  #:destination-dir destination-dir 
  #:settings settings 
  [#:fetch fetch 
  #:download download 
  #:silent-interval-seconds silent-interval]) 
 → updater-runtime?
  config : updater-config?
  destination-dir : path-string?
  settings : updater-settings-adapter?
  fetch : procedure? = fetch-update-manifest
  download : procedure? = download-update
  silent-interval : real? = silent-check-interval-seconds
Builds the scaffolding. The default transport hooks are the real deadline-bounded updaters; tests and products with injected clients override them.

procedure

(updater-runtime-state runtime) → hash?

  runtime : updater-runtime?
The current phase snapshot: 'status drawn from "idle" "checking" "available" "downloading" "downloaded" "error", 'percent, 'message, and 'downloaded_path.

procedure

(updater-runtime-check! runtime) → (or/c #f update-candidate?)

  runtime : updater-runtime?
Fetches and selects against the config, drives the download when one is available, and writes the silent-check stamp because the check settled — whether it answered or raised. The error propagates with the phase recorded.

procedure

(updater-runtime-download! runtime    
  candidate) → path?
  runtime : updater-runtime?
  candidate : update-candidate?
Downloads the candidate into the destination directory with progress reflected in the phase box, verifies, and records the path.

procedure

(silent-check-due? runtime now-seconds) → boolean?

  runtime : updater-runtime?
  now-seconds : exact-positive-integer?
#t when no stamp exists or the interval has elapsed since the last settled check.

procedure

(mark-silent-checked! runtime now-seconds) → void?

  runtime : updater-runtime?
  now-seconds : exact-positive-integer?
Persists the settled-check stamp behind the settings adapter.

procedure

(draw-rollout-bucket) → (integer-in 0 99)

Draws one rollout bucket from the operating-system entropy pool; racket/base’s PRNG has a fixed seed and must not be used for rollout placement.

procedure

(resolve-rollout-bucket! settings [key]) → (integer-in 0 99)

  settings : updater-settings-adapter?
  key : (or/c string? symbol?) = "update.rollout-bucket"
Recovers this installation’s sticky rollout bucket from the settings adapter, drawing and persisting a fresh value on first run. A corrupt or out-of-range stored value redraws; a store that cannot persist degrades to an unpersisted draw. Pass the result to updater-config as its rollout-bucket.

procedure

(platform-installation? value) → boolean?

  value : any/c
Recognizes an opaque first-party portable replacement transaction. Its plan, health and commit callbacks, and target-scoped lock identity remain private so callers cannot bypass the installation invariants.

8.1 Versions and Channels🔗ℹ

procedure

(version? value) → boolean?

  value : any/c
Recognizes SemVer 2.0 version strings.

procedure

(version-compare left right) → (or/c -1 0 1)

  left : string?
  right : string?
Compares SemVer precedence; build metadata does not affect the result.

procedure

(version<? left right) → boolean?

  left : string?
  right : string?
Reports SemVer precedence; equivalent to a negative version-compare result.

procedure

(version<=? left right) → boolean?

  left : string?
  right : string?
SemVer <=.

procedure

(version=? left right) → boolean?

  left : string?
  right : string?
SemVer equality by precedence.

procedure

(version>=? left right) → boolean?

  left : string?
  right : string?
SemVer >=.

procedure

(version>? left right) → boolean?

  left : string?
  right : string?
SemVer >.

procedure

(valid-channel? value) → boolean?

  value : any/c
Recognizes 'stable, 'beta, and 'dev.

procedure

(channel-accepts-version? channel version) → boolean?

  channel : symbol?
  version : string?
Checks whether a version is allowed by a release channel.

8.2 Release Target Metadata🔗ℹ

These helpers live in the leaf module rivet/distribution/identity and are also re-exported by rivet/distribution. Requiring the leaf module does not initialize cryptographic factories or platform adapters.

procedure

(platform-symbol [os]) → (or/c 'windows 'macos 'linux)

  os : symbol? = (system-type 'os)
Maps Racket’s 'windows, 'macosx, and 'unix operating-system symbols to the exact platform vocabulary used by release manifests. Unsupported values fail instead of being classified as Linux.

procedure

(architecture-symbol [machine]) → (or/c 'x64 'arm64)

  machine : symbol? = (system-type 'arch)
Maps 'x86_64/'amd64 and 'aarch64/'arm64 to the manifest architecture vocabulary. Unsupported architectures fail instead of being classified as x64.

procedure

(installer-extension installer) → string?

  installer : symbol?
Returns the canonical filename extension for 'zip, 'targz, 'msi, 'msix, 'exe, 'dmg, 'pkg, 'deb, 'rpm, or 'appimage.

procedure

(rfc3339-now [seconds]) → string?

  seconds : real? = (current-seconds)
Formats seconds in UTC at second precision, using the RFC 3339 form stored in a manifest’s published-at field.

8.3 Signed Manifests🔗ℹ

update-artifact values describe one platform artifact using the fields platform, architecture, url, sha256, size, installer, and arguments. update-manifest values contain the signed application and release policy plus a non-empty list of artifacts. The minimum updatable version and optional rollback predecessor must both be older than the release version. Artifact sizes are positive, and a manifest cannot repeat the same platform, architecture, and installer identity. Payloads and signed wrappers contain exactly one JSON value and reject fields outside their versioned schema, so a publisher typo cannot be silently ignored.

procedure

(write-signed-manifest manifest    
  private-key    
  key-id    
  [out]) → void?
  manifest : update-manifest?
  private-key : any/c
  key-id : string?
  out : output-port? = (current-output-port)
Validates, serializes, and signs a manifest payload with Ed25519.

procedure

(read-signed-manifest input)

 → 
update-manifest? bytes? string? bytes?
  input : input-port?
Decodes a signed wrapper for inspection and returns its parsed manifest, exact payload bytes, key ID, and signature bytes. This procedure does not authenticate them; update clients should use verify-signed-manifest.

procedure

(verify-signed-manifest input 
  public-key 
  [#:key-id expected-key-id]) 
 → update-manifest?
  input : input-port?
  public-key : any/c
  expected-key-id : (or/c #f string?) = #f
Verifies the expected key ID and Ed25519 signature over the exact payload bytes before parsing or exposing the manifest.

The versioned schema tag carried by every manifest payload.

procedure

(manifest->payload-bytes manifest) → bytes?

  manifest : update-manifest?
Serializes a manifest into the exact canonical payload bytes a signature covers. Publishers sign these bytes; there is no second serialization.

procedure

(payload-bytes->manifest payload) → update-manifest?

  payload : bytes?
Parses canonical payload bytes into a manifest. The closed schema rejects unknown fields, trailing JSON values, and contradictory policy, whether or not the payload was authenticated.

8.4 Key and Digest Primitives🔗ℹ

The same primitives the updater uses, exposed for release tooling and tests. Keys are Racket crypto key values; readers accept the standard DER encodings (SubjectPublicKeyInfo, OneAsymmetricKey, PrivateKeyInfo) and Rivet’s textual forms.

procedure

(read-ed25519-public-key path) → any/c

  path : path-string?
Reads and validates an Ed25519 public key from a file.

procedure

(read-ed25519-private-key path) → any/c

  path : path-string?
Reads and validates an Ed25519 private key from a file.

procedure

(bytes->ed25519-public-key raw) → any/c

  raw : bytes?
Decodes an embedded public key from bytes, the form release builds embed in application source.

procedure

(ed25519-sign private-key message) → bytes?

  private-key : any/c
  message : bytes?
Signs message with Ed25519 and returns the 64-byte signature.

procedure

(ed25519-verify public-key    
  message    
  signature) → boolean?
  public-key : any/c
  message : bytes?
  signature : bytes?
Verifies an Ed25519 signature; #f for any malformed signature.

procedure

(sha256-bytes input) → bytes?

  input : bytes?
Returns the raw SHA-256 digest of a byte string.

procedure

(sha256-file path) → bytes?

  path : path-string?
Returns the raw SHA-256 digest of a file’s exact bytes.

procedure

(sha256-file/hex path) → string?

  path : path-string?
Returns the lowercase hexadecimal SHA-256 digest of a file, the form signed manifests carry.

procedure

(bytes->base64-string value) → string?

  value : bytes?
Standard base64 encoding, used by signed wrappers and embedded keys.

procedure

(base64-string->bytes value) → bytes?

  value : string?
Decodes standard base64, raising on malformed input.

8.5 Selecting and Installing an Update🔗ℹ

An updater-config records the application ID, current version, channel, platform, architecture, public key, expected key ID, deterministic rollout bucket, and maximum download size.

procedure

(fetch-update-manifest 
  manifest-url 
  public-key 
  [#:key-id key-id 
  #:maximum-bytes maximum-bytes 
  #:connect-timeout-seconds connect-timeout-seconds 
  #:idle-timeout-seconds idle-timeout-seconds]) 
 → update-manifest?
  manifest-url : string?
  public-key : any/c
  key-id : (or/c #f string?) = #f
  maximum-bytes : exact-positive-integer? = (* 1024 1024)
  connect-timeout-seconds : (real-in 0.001 3600) = 30
  idle-timeout-seconds : (real-in 0.001 3600) = 60
Downloads a bounded manifest from HTTPS and verifies its Ed25519 signature. The blocking connect and the body read run inside a custodian-owned worker: a peer that never completes TLS or stops sending bytes raises a retryable error after the configured connect and idle deadlines instead of parking the caller.

procedure

(fetch-sibling-asset 
  url 
  destination 
  [#:sha256 expected-sha256 
  #:maximum-bytes maximum-bytes 
  #:on-progress on-progress 
  #:connect-timeout-seconds connect-timeout-seconds 
  #:idle-timeout-seconds idle-timeout-seconds]) 
 → path?
  url : string?
  destination : path-string?
  expected-sha256 : (or/c #f string?) = #f
  maximum-bytes : exact-positive-integer? = (* 1024 1024 1024)
  on-progress : procedure? = void
  connect-timeout-seconds : (real-in 0.001 3600) = 30
  idle-timeout-seconds : (real-in 0.001 3600) = 60
Downloads a release sidecar or sibling artifact next to the update feed. The response status is checked before anything is saved, a declared Content-Length beyond the byte limit is rejected, the stream honors the stall deadline, and an expected SHA-256 is verified before the download is moved into place. A failed checksum leaves no output file.

procedure

(select-update config manifest) → (or/c #f update-candidate?)

  config : updater-config?
  manifest : update-manifest?
Applies identity, channel, version, minimum-version, rollout, platform, and architecture policy.

procedure

(download-update 
  config 
  candidate 
  destination 
  [#:on-progress on-progress 
  #:progress-every progress-every 
  #:stall-timeout-seconds stall-timeout-seconds 
  #:total-timeout-seconds total-timeout-seconds]) 
 → path?
  config : updater-config?
  candidate : update-candidate?
  destination : path-string?
  on-progress : procedure? = void
  progress-every : (or/c #f exact-positive-integer?) = #f
  stall-timeout-seconds : (real-in 0.001 3600) = 60
  total-timeout-seconds : (or/c #f (real-in 0.001 86400)) = #f
Downloads to a partial file, enforces the signed size and configured bound, verifies SHA-256, and atomically moves the verified artifact into place. A peer that sends no bytes for stall-timeout-seconds raises a retryable error; total-timeout-seconds caps the whole transfer. on-progress receives cumulative downloaded bytes, starting at zero and ending at the downloaded size. Intermediate reports are throttled to about one per percent of the signed artifact size. The callback runs synchronously on the downloading thread and should quickly dispatch or record UI work. A final byte count reports transfer progress, not successful verification. Pass progress-every to select a byte cadence; #f uses the one-percent default. Small cadences are appropriate for an activity watchdog, not direct UI rendering.

procedure

(verify-update-artifact! candidate path) → path-string?

  candidate : update-candidate?
  path : path-string?
Checks the downloaded file’s byte size and SHA-256 digest against the signed manifest and returns path. Used by custom install flows; download-update already applies it before moving a verified artifact into place.

procedure

(make-install-plan candidate    
  downloaded-path    
  [#:backup-path backup-path]    
  #:install install    
  [#:restart restart    
  #:rollback rollback]) → install-plan?
  candidate : update-candidate?
  downloaded-path : path-string?
  backup-path : (or/c #f path-string?) = #f
  install : procedure?
  restart : procedure? = void
  rollback : procedure? = void
Builds the plan consumed by execute-install-plan! for product-specific native installers. Rivet’s portable adapter constructs its own plan through prepare-platform-installation.

8.6 Managed Check and Download Lifecycle🔗ℹ

procedure

(current-update-architecture) → (or/c 'x64 'arm64)

Returns the architecture symbol used by Rivet release manifests.

procedure

(make-updater #:application-id application-id 
  #:current-version current-version 
  [#:channel channel 
  #:platform platform 
  #:architecture architecture 
  #:install-kind install-kind] 
  #:public-key public-key 
  #:key-id key-id 
  #:manifest-url manifest-url 
  #:data-directory data-directory 
  #:rollout-bucket rollout-bucket 
  [#:maximum-manifest-bytes maximum-manifest-bytes 
  #:maximum-download-bytes maximum-download-bytes 
  #:check-timeout check-timeout 
  #:stall-timeout stall-timeout 
  #:total-timeout total-timeout]) 
 → updater?
  application-id : string?
  current-version : string?
  channel : (or/c 'stable 'beta 'dev) = 'stable
  platform : (or/c 'windows 'macos 'linux)
   = (current-update-platform)
  architecture : (or/c 'x64 'arm64)
   = (current-update-architecture)
  install-kind : (or/c #f 'portable 'msi 'appimage 'package-manager)
   = #f
  public-key : any/c
  key-id : string?
  manifest-url : string?
  data-directory : path-string?
  rollout-bucket : (integer-in 0 99)
  maximum-manifest-bytes : exact-positive-integer?
   = (* 1024 1024)
  maximum-download-bytes : exact-positive-integer?
   = (* 512 1024 1024)
  check-timeout : (and/c real? positive?) = 30.0
  stall-timeout : (and/c real? positive?) = 120.0
  total-timeout : (and/c real? positive?) = (* 30.0 60.0)
Creates an opaque updater that centralizes signed-manifest checking, candidate selection, bounded background download, progress, cancellation, and terminal state. public-key may be a parsed Ed25519 key or DER bytes, which are decoded lazily during the first check. rollout-bucket must be a stable per-installation value from 0 through 99; do not choose a new random value on each check. Unsupported platforms and architectures fail at construction instead of silently selecting the wrong artifact. Installation and restart remain owned by the native host or platform adapter.

On Windows, pass 'msi for an MSI-installed copy; release manifests carry a signed MSI entry alongside the portable ZIP, and the linked host library’s rivet::update::MsiHandoff owns the bounded quit/install/relaunch step. Linux AppImage copies pass 'appimage and use the platform adapter for the atomic replacement.

procedure

(updater? value) → boolean?

  value : any/c
Recognizes an updater created by make-updater.

procedure

(updater-check! instance) → hash?

  instance : updater?
Synchronously fetches and authenticates the manifest under check-timeout, selects the matching candidate, and returns a JSON-safe immutable result whose 'status is "available", "up-to-date", or "error".

procedure

(updater-download! instance) → void?

  instance : updater?
Starts the selected download on one background worker. A second check or download is rejected while an operation is active. Byte activity is monitored independently from UI percentage changes; exceeding the no-progress or total deadline interrupts the worker, removes its partial file, and publishes an error state.

procedure

(updater-cancel! instance) → boolean?

  instance : updater?
Requests cancellation of an active download. Returns #t when a worker was interrupted and #f when no download was active.

procedure

(updater-state instance) → hash?

  instance : updater?
Returns an immutable, synchronized, JSON-safe snapshot with phase, percent, message, downloaded path, available version, and current version fields. Contained strings and JSON collections are immutable as well as the top-level hash. The stable phase vocabulary is "idle", "checking", "available", "up-to-date", "downloading", "downloaded", "cancelled", and "error". A failed or cancelled download retains its authenticated candidate so an application may explicitly retry; starting a new check resets it.

procedure

(updater-wait instance [timeout]) → (or/c #f hash?)

  instance : updater?
  timeout : (or/c #f (and/c real? (not/c negative?))) = #f
Waits for the active download and returns its terminal snapshot. Returns #f when a finite timeout expires.

procedure

(execute-install-plan! plan    
  [#:health-check health-check    
  #:commit commit    
  #:journal-path journal-path]) → any/c
  plan : install-plan?
  health-check : (-> any/c) = (lambda () #t)
  commit : (-> any/c) = void
  journal-path : (or/c #f path-string?) = #f
Runs the native installation and restart callbacks, then requires health-check to return a true value before committing. The return value is the restart callback’s result, preserving the original procedure contract. If any step fails and the signed policy allows rollback, the rollback callback runs before the exception is re-raised.

When journal-path is provided, every destructive phase is written to an atomically replaced JSON journal. The restart callback must start the replacement and return so the health check and commit can complete. Use an absolute downloaded path and make rollback idempotent when durable recovery is enabled.

procedure

(recover-install-plan! plan 
  journal-path 
  [#:commit commit]) 
 → (or/c 'committed 'rolled-back)
  plan : install-plan?
  journal-path : path-string?
  commit : (-> any/c) = void
Recovers an interrupted journal for the exact same signed candidate, download, and backup paths. A transaction already marked healthy commits; any other unfinished phase rolls back when the signed manifest permits it. A mismatched plan, malformed journal, forbidden rollback, or failed rollback is rejected without deleting the evidence needed for diagnosis or another recovery attempt.

8.7 First-Party Platform Replacement🔗ℹ

procedure

(current-update-platform) → (or/c 'windows 'macos 'linux)

Returns the update-platform symbol for the running operating system.

procedure

(current-install-kind [executable])

 → (or/c 'portable 'appimage 'package-manager)
  executable : (or/c #f path-string?)
   = (find-system-path 'run-file)
Classifies the running installation for artifact selection: AppImage builds detect their APPIMAGE environment, deb and rpm installations resolve under "/opt" or "/usr", and everything else is portable. On Windows and macOS this always reports 'portable. Pass #:install-kind from this procedure to select-update or make-updater so a machine never selects a payload its installation cannot own.

procedure

(platform-installer-policy platform 
  installer) 
 → (or/c 'portable 'package-manager 'unsupported)
  platform : symbol?
  installer : symbol?
Classifies the signed artifact. Portable ZIPs (and Linux AppImages) may use Rivet’s atomic replacement adapter. MSI, MSIX, DMG, PKG, deb, and rpm remain owned by the native installer or package manager, so the portable adapter will not silently bypass elevation, receipts, or repository policy.

procedure

(verify-platform-payload! platform payload) → void?

  platform : symbol?
  payload : path-string?
Applies the platform trust gate to a staged portable payload: Authenticode on Windows, deep strict code-signature verification on macOS, and the already verified signed-manifest size/SHA-256 boundary on Linux.

procedure

(prepare-platform-installation candidate 
  downloaded-path 
  #:target target-path 
  #:restart restart 
  #:health-check health-check 
  [#:verify-staged verify-staged 
  #:backup-path backup-path]) 
 → platform-installation?
  candidate : update-candidate?
  downloaded-path : path-string?
  target-path : complete-path?
  restart : procedure?
  health-check : procedure?
  verify-staged : (or/c #f procedure?) = #f
  backup-path : (or/c #f complete-path?) = #f
Builds a same-filesystem, atomic portable replacement. The verified archive is staged and platform-verified before the old target moves. A durable marker distinguishes first installation from replacement, so rollback is safe in every interruption window. Target, download, and backup paths must be absolute and distinct. The verifier override is intended for development artifacts and tests; production callers should use the default fail-closed verifier.

procedure

(execute-platform-installation! installation 
  [#:journal-path journal-path]) 
 → any/c
  installation : platform-installation?
  journal-path : (or/c #f path-string?) = #f
Executes the prepared replacement with its restart, health, rollback, and commit callbacks. A backup is deleted only after health succeeds. Only one thread or process may execute or recover an installation at a time; a competing transaction fails before touching shared filesystem state.

procedure

(recover-platform-installation! installation 
  journal-path) 
 → (or/c 'committed 'rolled-back)
  installation : platform-installation?
  journal-path : path-string?
Recovers the exact prepared replacement. Interrupted commits are repeated; earlier phases restore the previous payload when signed policy permits it. The same installation-scoped lock used by execution protects recovery.