ADR 0004: managed launcher that deploys a local copy from a central source

Date: 2026-10-28. Builds on ADR 0002, which requires the app executable to be a local file.

Context

Companies that deploy the app centrally want one source of truth, such as a file share, without every user running the GUI app straight from it. Running from the share is undesirable:

The taskbar discovery helper restarts the same exe file (ADR 0002), so the exe must be local. People installing the app themselves use the normal installer or the portable ZIP and never see the launcher.

Decision

A small, separate launcher executable, shipped in a managed ZIP only, gives admins one central source while users always run a local executable. It resolves the source, refreshes the local copy when that is safe, starts the app and exits.

Responsibilities

When updates happen: only at launch and login

Configuration: no admin config file

Mixing installs is unsupported, and enforced both ways

The installer and the launcher use the same folder, so they must never both manage it:

Update rules

Login startup: the user’s choice in the app’s Settings

Rejected alternatives

Rules this changes

These take effect with the implementation, not with this record, because the docs describe the app as it is now. Each lands in the pull request that makes it true:

Part of #88. Implemented in #94 and #96.

Local inventory extension (2026-11-03)

Administrator opt-in is an optional elevenclock-rs-launcher.json beside the central app and launcher, writable only by the deployment administrators and readable by users. Schema version 1 requires local_inventory_enabled: true. This intentionally extends the original “no admin config file” decision for inventory only: source, destination and startup rules stay as above. Optional report_directory selects a drop box: one <SID>.<installation-ID>.json file per installation, written in place into a create-only folder, because every account may report and no folder can be provisioned per user. The app never creates directories on the share or changes permissions; executable/configuration access stays read-only. Nothing is read back. The summary command trusts a flat drop-box report only when its file owner matches the SID inside it, unless --skip-owner-check is given. Reports in subfolders (an older layout) are not owner-checked. The central release version or hash is not collected, so update progress is each installation’s own local version as a share of all reports, relative to the newest version observed. The configuration is not copied or cached locally. Missing/invalid/unreachable configuration disables collection, including offline launches; administrators can revoke it by setting false or removing the central file. No HKLM policy or device administration rights are required.

After the app start attempt, a job-owned local launcher helper checks the central configuration before collecting and again before saving one local snapshot. The three-second deadline covers blocking configuration reads as well as account lookup and local I/O, with bounded teardown and kill-on-parent-loss. The app owns no inventory timers or resources. Existing snapshots survive revocation/uninstall until explicitly removed. When report_directory is configured, the local snapshot is committed first and then published as <root>/<SID>.<installation-ID>.json inside the same bounded helper. One file per installation bounds growth across timeouts. Revocation or a destination change aborts publication on the next configuration check; in-flight server operations are not guaranteed cancellable. See local inventory for schema, disclosure, folder setup, offline behavior, rewrite rights and retention. Per-user report directories, append history and automatic permission management are not implemented.

The optional include_clock_format boolean, default false, extends inventory with committed format source only, including user-supplied literals. It shares the same explicit central configuration, helper lifetime and publication checks; it does not add app reporting, preference writes or vendor telemetry. Aggregation counts latest user/installation adoption per exact normalized format to inform future presets.

Inventory also includes latest-observation support codes and fixed severity levels under the same activation gate, without raw log uploads or another setting. Launcher update/start failures, fallback, source-state failures and fact/preference lookup warnings are structured independently of human logs. Local publication status starts as pending; centrally visible reports omit pending, while observed publication failures can be retained locally. A deadlined helper may leave pending status. Support aggregation counts affected users/installations, not event frequency. No app runtime monitoring, crash collection, event history or new worker is introduced.

The executing local launcher can invoke itself as the inventory helper even when refresh fails or is skipped: its own code implements that helper role. Share/staging launches require evidence that the local launcher matches the refresh source (matching hash, local-file source, or successful replacement), and otherwise skip helper invocation, preserving the app start result and previous snapshot. This freshness evidence does not negotiate helper-protocol support or define a minimum compatible release. Keep compatible managed app/launcher artifacts together. Unreadable configuration still disables collection; local launcher logs and stale last-seen timestamps remain the fallback when reporting cannot run.

An explicit --inventory-summary <report-root> <output.json> [recent-days] launcher mode aggregates existing reports using the portable library. It performs no deployment, app start, registry changes or new fact collection. It writes an atomic JSON summary outside the report tree, with distinct-user/installation counts, format adoption, support issues and latest snapshots. This uses the existing shipped managed launcher; no additional executable, runtime, service or scheduler is shipped.