Skip to content

v3.0.0 - #88

Merged
almenscorner merged 112 commits into
mainfrom
dev
Sep 28, 2026
Merged

almenscorner merged 112 commits into
mainfrom
dev

Conversation

@almenscorner

@almenscorner almenscorner commented Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator

Support Companion 3.0.0

Full detail in CHANGELOG.md.

Highlights

Fleet mode. Self-service app catalog, device compliance with per-policy
remediation steps, host details, and in-app Fleet Desktop SSO sign-in. Selected
automatically on Macs running orbit when no other mode matches.

Jamf patch reporting. Pending application patches from Self Service+ surface
in the tray menu and the app, reported against the policy that installed the
app rather than by name matching.

User installs. A standard user can install an administrator-allowlisted
.pkg or .dmg without holding admin rights. The helper re-reads the policy
and re-derives the installer's facts before acting, so the confirmation sheet is
presentation only.

Elevation is enforced by the helper, not the app. Deadlines and timers moved
into a root-owned directory, so demotion happens whether or not the app is
running — including catching up on a deadline that passed while the Mac was off.
Admin rights granted to other accounts during a window are revoked, more than
one user can be elevated at once, and reasons are logged where the user cannot
edit them.

Declarative helper deployment. With
com.apple.configuration.services.background-tasks (macOS 15+, supervised) the
helper and its launchd job land in a managed directory that even root cannot
write, and the job cannot be unloaded — so an elevated user cannot stop the
process that will demote them. Recommended wherever EnableElevation is used.

Also: Apps page grouped by available updates and searchable, elevation
countdown next to the tray icon, migration to the Observation framework, macOS
27 fixes (battery health, temperature, time to full, tray popover behaviour),
and translations for German, French, Japanese, Norwegian and Swedish.

Breaking changes — action required

Read the CHANGELOG's Breaking changes section before deploying. In short:

  1. Privileged settings are only honored from a configuration profile.
    IsPrivileged, RequirePrivilegedActionAuthentication and every elevation
    key are ignored anywhere else — including /Library/Preferences, because
    only root can write it and that is exactly the problem. If you deploy these
    with defaults write, move them to a profile.
    Ignored keys are logged, so
    a setting that appears to have stopped working can be confirmed from the log.
  2. The helper only serves 3.0.0 and later clients, must be signed with the
    hardened runtime, and must run from /Applications/SupportCompanion.app.
  3. The package installs the helper itself and replaces it on every install,
    failing the install if it does not end up running. A new app talking to an
    old helper is not merely stale — every privileged operation fails.
  4. Fleet mode is selected automatically on Macs with orbit installed when no
    other mode matches. Set Mode explicitly to keep a different one.

EnforceAdminAllowlist defaults to off. Before turning it on, PermanentAdmins
must list every account that should keep admin rights, including management and
break-glass accounts and anything granted rights by Platform SSO — anything not
on the list is demoted within five minutes.

Testing

  • 116 unit tests across Fleet models and managers, installer policy, Jamf
    update matching, catalog matching and SSO.
  • CI now runs the suite before it signs anything; previously no workflow ran it.
  • Release tags now point at the commit they were built from
    (target_commitish), rather than at the default branch.

Documentation

The wiki is rewritten for this release and goes live with it: new pages for
Fleet mode, Admin elevation, User installs, Deploying the helper, Modes,
Troubleshooting and a full preference reference.

Closes #78, closes #80

almenscorner and others added 30 commits October 20, 2025 16:05
- Introduced `PendingJamfUpdate` model to handle pending updates from Jamf.
- Created `PendingJamfUpdatesManager` to manage fetching and processing of Jamf updates.
- Updated `AppStateManager` to include Jamf updates management.
- Enhanced `ApplicationsInfoManager` to retrieve installed Jamf applications.
- Modified `AppCard` to load icons for Jamf applications asynchronously.
- Updated UI components (`PendingUpdatesCard`, `PatchingProgressCard`) to display Jamf updates.
- Added `InfoHelp` view for contextual help with icons.
- Enhanced `Preferences` to include settings for Jamf integration.
- Improved handling of application icons and action texts for installed apps.
…ling and enhance CircularProgressWithWave for smoother wave animation
- Implement JamfInfo and JamfInfoManager for managing Jamf data.
- Introduce async functions to fetch last check-in, last inventory update, and Jamf URL.
- Update AppStateManager to handle Jamf ID and integrate JamfInfoManager.
- Create CompactJamfInfoCard for displaying Jamf information in the UI.
- Refresh Jamf info on relevant state changes in CardGrid and TrayMenu.
Code quality, security and macOS 27 fixes
Fleet mode: self-service apps, compliance and notifications
Merge the unreleased Fleet changes and the never-released 2.4.0 into 3.0.0,
with breaking changes first: privileged settings only from administrators,
automatic Fleet mode on Macs with orbit, and the SMAppService helper. Add
the code quality and macOS 27 fixes.

Set the app's version to 3.0.0 in Info.plist, which build.zsh reads, and in
MARKETING_VERSION, which Xcode builds use.
The pre-release workflow failed with exit code 65 after the Fleet merge.
FleetIconCatalog used regex literals, which Xcode 26 only accepts in Swift 5
mode with a compiler flag the project doesn't set; use NSRegularExpression.

- Build releases with Xcode 26.6 instead of 26.0.1 on the macos-26 runner
- Show xcodebuild's output when archiving, so failures in CI include the
  actual error
Xcode 26 couldn't infer .orangeLight in a foregroundStyle ternary, since
foregroundStyle takes any ShapeStyle. Name the Color type explicitly.
Compliance was a card on Home competing with the device cards for space.
It now has a sidebar page carrying the failing-checks badge, with a status
hero, severity-tinted cards whose resolution is expanded by default, and
passing checks compressed into a grid. The Home banner and the tray card
route there via supportcompanion://compliance.

The Apps cards reserved worst-case height for states most never enter, and
repeated "Not installed" under a header already saying Available. They now
size to content, badge only informative states, and use a tinted capsule
rather than a grid of solid buttons. Search is capped and shares a row with
the filter chips, which now follow the branded accent instead of the system
one.
computeUpdates matched a patch to any Self Service policy and then judged it
on dates and versions alone, so apps merely offered in Self Service were
reported as pending updates. GIMP showed as a pending patch on a Mac without
it installed: the policy advertises 3.2.4 in its description while the patch
is 3.2.0, and with no install date and no deadline the version mismatch alone
marked it due.

Skip policies that are not installStatus 4, matching what
getInstalledJamfAppsFromStore already does. Not installed also no longer
counts toward upToDateCount, consistent with the fallback counters below it.
The gradient only survived one of the four appearance modes; tinted discards
it and dark dropped it to 0.7 opacity with a lighten blend. A solid fill with
a white glyph holds its silhouette at menu bar and Dock sizes, and lets the
system supply the specular.

The README opener described the app as empowering end-users without saying
what it shows, which is also what the repo description and cask desc say.
The embedded web view was sized from its own content while the page
reflowed to whatever size it was given, so each layout pass changed the
size that drove the next one. SwiftUI reported the resulting dependency
cycles and eventually crashed in StackLayout.makeChildren. The view now
returns the size it is offered from sizeThatFits, so nothing asks the
web view how large it wants to be.

Its navigation delegate was also being replaced. WebViewState is the
delegate and defers its loading-state updates to the main queue to avoid
writing observable state mid-update; the coordinator that replaced it
wrote synchronously. The coordinator is gone and the deferred delegate
stays in place. The loading indicator is hidden rather than removed, so
a page load cannot change the stack's children during its own layout.

Removes WebView.swift, whose WebViewWrapper had no remaining references.
The URL was built by interpolating the action name, so a name containing
&, # or a space produced a URL that did not round-trip and the app either
ran the wrong action or none at all. Built with URLComponents instead.
Every restriction on what could run as root was enforced in the app. The
helper's interface was a pair of methods that ran any command or script,
so code running in the app process was equivalent to root, and any build
we have ever signed satisfied the helper's client check — keeping an
older copy on disk was enough to reach it.

The helper now decides.

Client validation: the connecting app must be 3.0.0 or later, signed with
the hardened runtime, and running from /Applications/SupportCompanion.app.
The signature is verified before anything is read out of the signing
information, and the caller's uid comes from the audit token rather than
from the client.

Interface: named operations instead of commands. For a privileged action
the app sends only the action's name and the helper looks the command up
in administrator-managed preferences itself. EnableElevation is re-checked
in the helper rather than only hiding a button. executeScript, which had
no callers, is gone.

Elevation: the deadline and the demotion timer live in a root-owned file
under /var/db, so quitting the app or deleting a preference no longer
leaves a user an administrator. Demotion happens whether or not the app
runs, including catching up after the Mac was off. Several users can be
elevated at once. While a window is open the admin group is watched, by
name and by UUID, and rights granted to anyone else are taken back.

EnforceAdminAllowlist and PermanentAdmins close what remains: a user with
root can delete the state file, so a missing deadline must not mean there
is nothing to do. With an allowlist set, an administrator the allowlist
cannot explain is demoted, which makes deleting the state the losing move.
Off by default, refused outright when PermanentAdmins is absent, and
members that cannot be resolved to an account are reported, not removed.

Deployment: the helper can be delivered with a background-tasks
declaration, which puts it somewhere root cannot touch and stops an
elevated user killing the process that will demote them. SkipHelperInstall
keeps the package from installing a competing copy. DDM/make_ddm_assets.zsh
builds the assets.

The package now replaces the helper on every install and fails the install
if it does not end up running, since an old helper no longer merely lags
the app — it cannot serve it at all. Uninstalling demotes anyone still
elevated before removing the helper that would have done it.
The installed and available versions sat in their own stack below the
icon row, which left them detached from the title they describe and put
a gap between the two blocks. They now sit under the title inside the
same row, with the icon centred against the group.
Extracted by Xcode for strings the Fleet compliance and app catalog work
already referenced. 81 new keys, English only — the Japanese, Norwegian,
Swedish, German and French localizations do not cover them yet.
Fleet 4.92 ships fleet_desktop.sso_enabled, so the sign-in that was previously
deferred can now be done in the app instead of sending the user to a browser.

Sign-in runs in a web view: the app asks Fleet where to send the user, plants
Fleet's handshake cookie in the web view's cookie store, and shows the identity
provider's own page. Only the session Fleet issues at the end reaches the app.
That cookie is HttpOnly, so it is read from WKHTTPCookieStore rather than from
the page, and kept in the keychain scoped to the Fleet host that issued it. The
session is restored on the way into the first gated request rather than at
startup: a request that got ahead of the restore would come back sso_required,
and that answer discards the stored session.

Fleet's /device/{token}/desktop endpoint sits outside the SSO gate and still
reports how many policies fail, so the sidebar badge, the compliance cards and
the notification stay truthful while signed out. Everything Fleet withholds
until sign-in now collapses to a single Sign In action rather than a column of
Unknown.

FleetNotifySignIn (default off) notifies the user when a sign-in is needed, at
most once a day, leading with the failing count where there is one. The tray
cards route through supportcompanion://fleetsignin, because the sheet belongs to
the main window and presenting it from the menu bar popover does nothing when no
window is open.
Lets a standard user install software they have downloaded, without handing
them an admin password, when an administrator has allowlisted it.

The helper owns the decision. It re-reads the policy and the allowlist before
acting, inspects the package or disk image itself, and only then stages and
installs. Nothing the app says about an installer is taken on trust, so a
tampered client cannot talk the helper into running something the policy does
not allow. EnableUserInstalls is off by default, and the settings that gate it
are read only when an administrator set them.

Installers can be opened from the app, from Finder's context menu, or by
double-clicking when the app is registered for the type. Where the catalog
already offers the same application, the user is pointed at the approved
version instead, and where the policy refuses an installer outright they can
elevate and run it themselves if elevation is available to them.

handleElevation takes an optional completion handler so an install can carry on
once rights are granted, and AppStateManager now uses the shared
ElevationManager rather than its own instance, so the countdown the UI reads is
the one the install flow started.
…ebar

The string catalog gains German, French, Japanese, Norwegian and Swedish for
the 85 strings that were still English only.

The patching progress wave takes the configured accent colour instead of the
system one, so it matches the rest of a branded install. The dark mode toggle
and the sidebar rows get their spacing and highlight tidied up.

Date.relativeDescription replaces the formatted(.relative:) calls in the Fleet
cards, which produced "in 0 seconds" for a date that had just passed.

Sharpens the CHANGELOG wording on the privileged settings and helper version
checks, which described the fix rather than what it prevented.
…uration profiles and improve clarity in handling SkipHelperInstall settings.
…cements, including MDM profile requirements and user install features.
A pending Zoom update was never reported. Jamf lists a patch title as a policy
of its own beside the policy that actually installed the app, under different
names: "Zoom Client for Meetings" with installstatus 0, and "zoom.us" with
installstatus 4. computeUpdates took an exact name match unconditionally, so it
picked the listing rather than the installation, and the installed-app check
added in 318d483 then discarded the patch.

Matching now scores every plausible policy and prefers the one with the better
evidence of an installation. A flat "is it installed" test cannot separate these
two: both names reduce to the token "zoom", so both resemble zoom.us.app equally
well, and only the install record tells them apart.

That evidence no longer comes from installstatus alone, which is wrong in both
directions. Apparency is installed but its policy reports 0, which hid its patch;
Brave reports 4 with the bundle long since deleted, which produced exactly the
phantom update 318d483 set out to prevent. The Mac now gets the veto and Jamf the
tie-break, with installstatus trusted outright only when the application
directories cannot be read at all — an empty index means the lookup failed, not
that the Mac has no applications.

Also stops a patch installed from here flipping back to Update. Self Service+'s
store is its own cache and only changes when it runs, so re-deriving the list
straight after an install read a store that still offered the patch, and clearing
the spinner left the row showing its default label. RecentlyInstalledPatches
holds it back until the store agrees, or until thirty minutes have passed so an
install that silently did nothing reappears rather than hiding for good. This
mirrors installedVersionsAwaitingInventory on the Fleet side, which covers the
same lag.

refreshSelfService now waits for the store's modification date to change instead
of sleeping two seconds and hoping, and asks Self Service+ to quit before
resorting to SIGKILL, which could cut off the write it is trying to read. A
Self Service+ window the user has open is left alone: quitting something
somebody is reading to refresh a cache is not a good trade, and the suppression
above covers correctness instead.

computeUpdates had no test coverage. It takes its installed-app index as a
parameter so the rules can be exercised without touching the disk.
The Apps page listed every installed application in one flat grid. The only
thing on it anybody has to act on is an available update, and that was findable
only by scrolling until a card turned up with an Update button on it.

Updates Available now comes first, then Installed, each with a count, and a
search field narrows both. Which apps have an update comes from the active
updates manager rather than from a check on the configured mode, through a new
installedAppName on PendingUpdate. Jamf needs it because the two names routinely
differ, and any other mode that starts reporting it gets the section without
this view changing.

Constants.Apps aliases the Fleet catalog's wording rather than adding keys of
its own. The text is word for word the same, and a second set would mean
translating "Installed" into five languages twice and keeping both in step.
The helper refuses any client not signed with the hardened runtime, because a
client without it can be injected into and used as a proxy. Nothing in the
project asked for it, though: hardening was applied only by build.zsh when
packaging a release, with --options runtime. An app built and run from Xcode
therefore had no runtime flag at all and every privileged operation failed with
"Client is not signed with the hardened runtime". verifyClientPolicy already
skips its install-path check under #if !DEBUG, for the same reason that a debug
build runs from DerivedData, but the runtime flag had no such allowance.

ENABLE_HARDENED_RUNTIME was set on the helper target only. Setting it on the app
as well means a debug build satisfies the helper the same way the shipped app
does, rather than by relaxing what the helper accepts.

That alone breaks xcodebuild test, and the error does not say so plainly: the
hardened runtime brings library validation with it, which refuses to load code
signed by a different team, and the test bundle signs ad hoc so it has no team
to compare. Tests fail to load before any of them runs.

SupportCompanion-Debug.entitlements carries
com.apple.security.cs.disable-library-validation and is used by the Debug
configuration only; Release keeps SupportCompanion.entitlements unchanged. The
cost is real and worth stating: it makes the debug build's hardened flag hollow,
which is the very property the helper is checking for. That is acceptable for a
build that only ever runs on a developer's own Mac, and the release build keeps
the full guarantee.

Signing the test bundle with the app's identity instead would preserve the
property, but Xcode refuses non-ad-hoc signing without DEVELOPMENT_TEAM, and the
only way to supply it is to commit a particular team into this file — which
would break every other contributor.
Updated app icon in README and added a new image.
NSWindow holds its delegate weakly, so the WindowDelegate constructed inline in
presentAsWindow was released the moment it was assigned, and windowWillClose
never fired. Nothing reaches it today: the window is borderless, so it has no
close button and performClose: will not act on it, and both buttons in
ReasonInputView call closeWindow() themselves. But it was the safety net for any
other way the window might go away, and presentAsWindow guards on window == nil,
so a close that skipped closeWindow() would leave that property set and the
elevation reason prompt unable to open again until the app restarted.

The manager now holds the delegate, and closeWindow clears its state before
closing rather than after. Once the delegate is actually retained, close() sends
windowWillClose synchronously, which routes straight back into closeWindow();
with window still set, that second pass would fall through the guard and close
the window twice.
No workflow ran the test suite. build.zsh only archives, so all three went
straight from importing certificates to building, signing and notarizing a
package nothing had tested.

The test step runs before the certificates are imported, so a failure stops the
run before it spends time on notarization. It overrides the signing settings
because the Debug configuration expects a Developer ID certificate the runner
does not have, while the tests need none. Debug is also the only configuration
that relaxes library validation, without which the hardened runtime refuses to
load the ad-hoc signed test bundle into the app.

action-gh-release creates the tag at the repository's default branch when
target_commitish is unset. Prereleases are dispatched from dev, so every tag it
has written points at main's head rather than the commit that was built: all
five v3.0.0.* tags and four of the v2.4.0.* ones resolve to 8586e93, from last
November. No shipped package was affected, but checking out a release tag does
not give you the source it was built from.
@almenscorner
almenscorner merged commit 3f3b869 into main Sep 28, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Backgrpound Software Updates Desktop Info - FileVault information not removing

2 participants