Hotfix/package manager install - #1927
Merged
Merged
Conversation
denys-gif
reviewed
Aug 27, 2026
denys-gif
reviewed
Aug 27, 2026
denys-gif
approved these changes
Aug 27, 2026
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.
Feature:
Make OpenFrame installable through package managers (Chocolatey, Homebrew, winget) by splitting installation from authentication into two independent steps.
A package cannot carry tenant parameters — Homebrew casks accept no install-time arguments at all, and a package that still requires values copied from the dashboard is no better than the existing one-liner. So the package installs a completely generic client, and enrollment happens afterwards as its own command:
step 1 — from the package manager, no parameters
choco install openframe -y | brew install --cask openframe | winget install openframe
step 2 — from the tenant dashboard (Devices → Add device)
openframe auth --serverUrl {tenant-name}.openframe.ai --initialKey --orgId --userId
This also unlocks deployment models the one-liner can't serve: baking an unenrolled client into a golden image (safe — no machine identity exists until enrollment), and MDM/RMM mass deployment where the configuration arrives separately. Installing with full parameters is unchanged and never needs auth.
Solution:
Parameterless install. InstallConfigParams::is_parameterless() (zero arguments — any partial set still fails validation, so a mangled command is caught rather than silently deferred) makes install skip build_and_save entirely. Binary copy, service registration and PATH wiring proceed as normal, and the run ends with a tenant-agnostic pointer to the dashboard plus a note that updates are managed by the platform.
Awaiting-auth gate in Service::run(), before Client::new(). The placement is load-bearing: the client reads initial_config.json during construction, so without the gate a parameterless install would fail in the constructor and crash-loop under SCM/launchd — a silent failure that looks like a broken install. Instead it logs one line and polls every 15s (AWAITING_AUTH_POLL_SECS) until a configuration appears. Release requires a file that both parses and carries a non-empty server_host, so a partially written file cannot release it.
New auth subcommand. Takes the same arguments as install (including optional --userId, --tag, --localMode) and runs doctor::run_auth: required arguments → admin → WebView2 → disk space → DNS/TCP/TLS/WebSocket probes against the supplied host. Nothing is written unless it passes; WebView2 is healed from that report, then the configuration is saved and the service restarted.
Two of those checks sit here deliberately. WebView2 is a chat-tool prerequisite, and tools only arrive after registration — so its natural home is the step immediately before provisioning, not the bare install. Disk space matters because the gap between install and auth can be months (golden image, pre-provisioned package) and tool provisioning starts as soon as auth succeeds.
The restart is not just about latency. Configuration is read once at startup, so re-running auth on an already-running client — typo'd host, tenant move, support asking for a re-run — would otherwise report success and change nothing until a reboot. Service::nudge_restart() is best-effort and silent; on failure the poll still picks the configuration up, which also covers config delivered by MDM file-drop rather than the CLI.
Diagnostics follow the step that needs them. Parameterless install runs run_preinstall_parameterless() — admin, bin dir, disk, the three data directories, service-config writability: exactly what installation touches. Argument, network and WebView2 checks move to auth, where the parameters finally exist. The full-parameter install keeps today's complete preinstall pass. All paths print the same report shape and share identical failure/warning handling, so a package manager's log always contains a named check table rather than a mid-operation error chain.
openframe alias. The package ID is openframe, so install creates that name beside openframe-client. On Windows it is a .cmd shim (@"%~dp0openframe-client.exe" %*) rather than a hard link — the updater replaces the binary by swapping its directory entry, which would leave a link serving the pre-update binary forever; %~dp0 resolves at run time, so the shim never goes stale. Unix uses a symlink, which already has that property. Removed on uninstall.
Supporting behavior. doctor reports a calm "Awaiting authentication" state on an unenrolled machine instead of a config-not-found failure; run (direct/dev mode) fails fast with a clear message rather than idling like the service.
Intentional, so reviewers don't flag them: re-running install on an enrolled machine still de-authenticates it — the internal reinstall uninstalls tools and clears app_support, while machine_id/client_secret survive so re-auth re-registers the same device. A deliberate reinstall is expected to go through both steps again. Package-manager upgrades are intentionally unsupported; agents update through the platform.
Validation: cargo fmt --check and clippy -D warnings clean; 140 tests, 135 passing (5 pre-existing ignored), including new coverage for the parameterless decision function and the gate's release condition. Behaviour needing real machines — service state while idling, launchd/SCM behaviour, the full install → auth → registration path — is tracked as post-review verification in possible_tickets/Working/CLIENT_PACKAGE_MANAGERS.
Review:
The openframe alias was lost on an install run from the installed location. create_alias and add_to_windows_path sat inside the current_exe_path != install_path branch. Running the already-installed binary's install takes this path: the existing-installation check launches the internal uninstall (REINSTALL_ENV=1), which removes the alias, and then the copy branch is skipped because the paths are equal — so the alias and PATH entry were never restored, and the "run openframe auth …" instruction printed right afterwards failed with command-not-found. Both calls now run unconditionally after the copy branch; both are idempotent (create_alias removes any existing alias first, add_to_windows_path already no-ops when the directory is present).
nudge_restart could leave the service stopped while auth reported success. If the stop succeeded and the start failed, both errors went to a file-only debug! while the CLI printed "Authentication saved" and exited 0. Nothing restarts an explicitly stopped service, so the device would never register — strictly worse than not restarting at all, since the awaiting-auth poll dies with the process. nudge_restart now returns Result and only starts what it actually stopped: a failed stop leaves the service running and returns Ok (the poll still applies the config), while a failed start propagates. The CLI then prints the platform-specific recovery command (sc start com.openframe.client / sudo launchctl kickstart -k system/com.openframe.client) and exits 1, so automation cannot mistake it for success. The configuration is saved either way, and re-running auth retries the restart.
doctor reported any config read error as "awaiting authentication". The Err(_) arm swallowed permission-denied, locked files and I/O errors alongside a genuinely missing file, so openframe doctor printed "All checks passed" and exited 0 on an agent that cannot start. Only ErrorKind::NotFound now yields the informational awaiting-authentication result; every other error kind is a failure carrying the path and the OS error.