Breeze RMM v0.103.0 — the 2026-07 security remediation program lands, alongside software-deployment visibility, alert-rule consolidation, and Microsoft 365 delegated communications.
⚠️ Read before upgrading
Three things need your attention:
- Four new required environment variables —
docker compose upwill refuse to start without them. - Remote access needs a short maintenance window, not a rolling restart.
- Raw device metrics older than 30 days will be deleted on first boot. History charts are unaffected; see below.
Summary
- Security remediation waves 2–7 — report tenant/site scope, durable live revocation, remote WebSocket ownership, agent mTLS identity, agent updater and outbound network trust, and data-export/audit hardening. Wave 1 shipped in 0.102.0. (#2840, #2841, #2842, #2843, #2872, #2933)
- Software deployment visibility — a Deployments tab, a repaired status pipeline, and bulk selection carried into the wizard. (#2876)
- Alert rules consolidated under the Alerts config-policy feature, so one rule set can be owned partner-wide. (#2946, #2969)
- Microsoft 365 delegated communications — consent, onboarding, mail-scoped action catalog, and the executor service. (#2880, #2926, #2940, #2943)
- RDS per-session helpers — session targeting, on-demand leases, and per-session consent on multi-session Windows hosts. (#2911)
- Device metrics retention — the raw metrics table no longer grows unbounded. (#2891)
Added
- Deployments tab for software deployment — see what actually shipped to which devices, with a corrected status pipeline and bulk selection carried through the wizard. (#2876)
- Microsoft 365 customer Graph actions — consent and onboarding, delegated-connection schema with RLS, mail-only action catalog with a frozen intent canonicalizer, plus intent binding, runtime config, and the communications executor service. (#2880, #2926, #2924, #2922, #2921, #2940, #2943)
- RDS per-session helpers — on multi-session Windows hosts, helpers are now targeted per session with on-demand leases and per-session consent, ending helper-process waste. Consent prompts now log which session received them. (#2911, #2941)
- Partner-wide alert rules — alert-rule ownership is consolidated under the Alerts config-policy feature. (#2946)
- Device Groups Compare bulk action, plus the page is reachable and crash-proof again. (#2944)
- Invoice editor on the quote save grammar — invoices and quotes share one save/validation path. (#2829)
- Backup helper logs — the backup helper ships real diagnostic logs instead of running dark. (#2801)
- Retention for
device_metricsandservice_process_check_results. (#2891) - Subscription cancellation when a partner is suspended for abuse (hosted only). (#2927)
- Durable-release guard and persisted origin principal for action intents (#2917); principal-kind discriminator on the auth context (#2915).
Improved
- Vulnerability tools registered with the chat MCP server, with CVE vocabulary. (#2812)
- Object-storage upload failures return actionable
502/503. (#2810) - Agent auth-dead backoff cap raised above the heartbeat interval. (#2793)
wingetscans with unreadable output reportErrScanSkippedinstead of a false clean result. (#2804)- Malformed UUIDs in webhook routes and cursors return
404instead of500. (#2918)
Fixed
- Remote terminal sessions died at 60 seconds — desktop orphan recovery was sweeping non-desktop sessions, with a silent UI freeze. (#2871)
- Terminal command collisions — execution serialized, command IDs de-collided. (#2889)
- Portal password leak — a pre-hydration GET submit could put the password in the URL. (#2884)
- Pending partners had no payment CTA, and
payment_method_attached_atwas trusted on unpaid subscriptions. (#2932) - Self-uninstall — the agent no longer kills its own service mid-teardown. (#2886)
- Windows disk inventory — one warning volume no longer discards the whole inventory. (#2945)
- Report wildcard grants —
*grants honored in report authority checks. (#2885) - Quote response capability — durable writer plus replay backstop. (#2888)
- Org lifecycle —
suspended→offboardingreachable via the API. (#2887) - Offboarding self-deadlock — entry path reuses the request transaction. (#2890)
- Partner-locked fields no longer
403every Org Defaults save. (#2811) - Org Defaults device-group select rendered raw i18n keys. (#2869)
- Linked Profiles tab shown on unlinked devices. (#2867)
- Toasts swallowed by island module duplication. (#2807)
- i18n interpolation for adjacent-JSX glued strings. (#2809)
- Portal accept-invite posted to the wrong route. (#2825)
- Partner-axis reads escaped to a system RLS context. (#2828)
- Three Sentry issues: authenticator FK 500s, S3 endpoint save gaps, CAS zero-row diagnostics. (#2912)
- Malformed S3 endpoints in
backup_configsnormalized. (#2916) - AI-minted temporary passwords sealed across both execution paths. (#2853)
- AI chat plan steps require durable tier-3 approval. (#2873)
Security
This release closes waves 2 through 7 of the 2026-07-23 security remediation program.
- Reports — tenant and site scope is enforced across the whole report authority and persisted with each report and run, so a site-restricted user cannot reach data outside their sites. (#2840)
- Authentication — durable live authorization and revocation. Permission and token changes take effect immediately rather than at next refresh. (#2841)
- Remote access — WebSocket session ownership held in a shared lease, with correct teardown and pre-upgrade authorization. (#2842)
- Agent identity — mTLS certificate binding, durable renewal and certificate history, edge transport trust. (#2872)
- Agent updater and network — manifest signing-key trust, outbound network policy, managed-software destination classification. (#2933)
- Data protection — export field minimization, export preflight classification, complete audit coverage. (#2843)
New enforcement in these waves ships switched off. Each has its own opt-in rollout — see below.
Self-hosters are encouraged to upgrade.
Self-Hosting / Upgrade Notes
1. New required environment variables
Four are now required. Compose stops with an error naming the missing one:
REMOTE_ACCESS_ADMISSION_MODE=open
REMOTE_WS_AUTH_MODE=post_upgrade
REMOTE_WS_REDIS_TOPOLOGY=standalone-single-primary
EVENT_PERMISSION_EPOCH_MODE=compat
These values are correct for a normal upgrade and are in .env.example. Add them to .env and confirm they're mapped in the api service's environment: block — a value in .env alone is not enough.
If you run MCP OAuth, one more:
OAUTH_AUTH_EPOCH_ENFORCE_AFTER=<absolute UTC timestamp, e.g. 2026-08-20T00:00:00Z>
Pick a time at least 30 minutes after you finish upgrading, and don't extend it later. This ends the compatibility window for access tokens issued before this release. .env.example ships a fixed example date — choose your own, or your MCP clients will be forced to re-authenticate immediately.
2. Raw device metrics older than 30 days will be deleted
device_metrics — the raw one-row-per-heartbeat table — has never had retention and has grown unbounded (#2827). This release adds a retention worker with a 30-day default. On its first run it prunes everything older than that, in bounded 10,000-row batches.
Your history charts are not affected. They are served from metric_rollups, which has always had its own retention and is untouched. What you lose is raw per-heartbeat detail beyond 30 days.
If you want to keep more, set DEVICE_METRICS_RETENTION_DAYS (clamped to 1–365) before starting the new version. The same applies to service_process_check_results.
If your device_metrics table is very large, expect the first prune to run for a while in the background. It's batched and will not lock the table.
3. Redis must be standalone single-primary
Remote access now coordinates session ownership through Redis and requires a single primary with AOF enabled, maxmemory-policy noeviction, and cluster mode disabled.
If you run Redis Sentinel, Redis Cluster, or a managed HA Redis with automatic failover, remote access will refuse to admit sessions after upgrading. This is deliberate: asynchronous replication cannot guarantee that only one server owns a session, and a split owner means two operators could drive the same machine. Everything else in Breeze continues to work normally.
If this affects you, move Redis to a single primary before upgrading.
4. Remote access requires a maintenance window, not a rolling restart
The first release with shared session ownership is a cutover. Old and new servers cannot both admit remote traffic.
Full detail in the upgrade guide:
- Set
REMOTE_ACCESS_ADMISSION_MODE=closedand recreate Caddy. Verify from outside the proxy that remote endpoints return503. - Stop every old API container. Confirm none are left.
- Wait 5 minutes with the barrier closed (60 seconds for WebSocket tickets, 300 for tunnel-HTTP tickets and cookies — the longer wins).
- Start the whole API pool on 0.103.0 with
REMOTE_WS_AUTH_MODE=post_upgrade. - Set
REMOTE_ACCESS_ADMISSION_MODE=openand recreate Caddy.
Expect roughly 5–10 minutes without remote access. Active sessions will be disconnected. Agents, heartbeats, and the web UI are unaffected.
Single-node installs still need this — the point is that no old API process is running when the new one starts.
5. Database
Eighteen migrations, all idempotent, applied automatically on API boot unless AUTO_MIGRATE=false. Take a backup first, as always.
Three deserve a specific mention:
- Never-firing alert rules are deleted. The old Monitoring tab offered a "Network Usage" metric that the threshold evaluator has no column for, so those rules have never fired once — same for a metric condition with no metric name. Single-condition rules matching that shape are removed. Multi-condition rules are left intact, and any rule referenced by an existing alert is counted, warned about in the Postgres log, and left alone. If you had such a rule, it will disappear from the Alerts tab.
- Two BRIN indexes are built on
device_metricsandservice_process_check_results. They're builtCONCURRENTLYoutside a transaction, so they do not lock the tables, but on a large install the build takes time. - One backfill classifies existing rows in
reportsandreport_runsas legacy-scoped and computes a hash per row. On an install with a long report-run history this adds time to the first boot.
No table rewrites otherwise.
6. Behavior changes and new switches
Alert-rule ownership moved. Alert rules are now owned by the Alerts config-policy feature rather than Monitoring. Existing rules are migrated; the practical change is that a rule can now be defined once at the partner level and applied across all your organizations.
Everything the security waves added is off by default. No fleet or user behavior changes on upgrade. Each of these is a separate decision with its own rollout — don't flip them on upgrade day:
| Variable | Default | What flipping it does |
|---|---|---|
AGENT_MTLS_BINDING_MODE
| off
| audit counts certificate/device mismatches without denying; enforce denies them. See the mTLS guide. Requires a validating proxy — leave off if you don't have one.
|
MANAGED_SOFTWARE_POLICY_MODE
| compat
| enforce requires every device to be on a 0.103.0-or-newer agent before it can run managed-software commands. Older agents are denied.
|
AGENT_REQUIRE_MANIFEST_SIGNING_KEY_ID
| false
| Requires update manifests to carry a signing key ID. Only set true once every update response includes one.
|
EVENT_PERMISSION_EPOCH_MODE
| compat
| enforce after all servers are on the new version and old writers have drained.
|
REMOTE_WS_AUTH_MODE
| post_upgrade
| pre_upgrade tightens WebSocket authorization, but only after every legacy viewer token has expired (2 hours after the last old server stops).
|
DEVICE_METRICS_RETENTION_DAYS
| 30
| Raw device-metric retention, 1–365. See section 2. |
Full procedure for the agent-side switches: docs/operations/agent-network-and-manifest-rollout.md.
7. Building from source
The Node pin moved to 22.23.2. If you build images yourself, update your toolchain. (#2937)
8. Upgrade command
# in /opt/breeze
cp .env .env.bak-pre-0.103.0
# add the new variables to .env (see above), then:
sed -i 's/^BREEZE_VERSION=.*/BREEZE_VERSION=0.103.0/' .env
docker compose pull api web portal
docker compose up -d binaries-init api web portal
curl -sf https://<your-domain>/healthCombine with the cutover in section 4 — close admission first, upgrade, then reopen.
No other breaking changes
Existing agents keep working without being upgraded. No API contract removed. No configuration removed.
Full Changelog: v0.102.0...v0.103.0
What's Changed
- fix(reports): enforce tenant and site scope across the report authority (security wave 2) by @ToddHebebrand in #2840
- fix(auth): durable live-authorization and revocation (security wave 3) by @ToddHebebrand in #2841
- fix(remote): websocket lifecycle ownership, teardown, and pre-upgrade authorization (security wave 4) by @ToddHebebrand in #2842
- fix(security): data classification, export preflight, and audit hardening (security wave 7) by @ToddHebebrand in #2843
- docs(security): 2026-07-23 remediation program — plans, containment, and wave records by @ToddHebebrand in #2851
- docs: document OFFBOARDING_DRAIN_WINDOW_HOURS (v0.102.0) by @ToddHebebrand in #2852
- docs: false-positive guidance for signed agent binaries + correct #1158 status by @ToddHebebrand in #2791
- test(api): pin the deployment_invites purge-cascade invariant — no exemption needed (#2821) by @ToddHebebrand in #2831
- fix(api): sweep raw Number(process.env.X ?? default) onto envInt + add a guard (#2823) by @ToddHebebrand in #2830
- fix(api): escape partner-axis reads to a system RLS context (#2822) by @ToddHebebrand in #2828
- chore(deps): bump the expo-sdk group with 5 updates by @dependabot[bot] in #2833
- chore(deps): bump @playwright/test from 1.61.1 to 1.62.0 in /e2e-tests by @dependabot[bot] in #2835
- chore(deps): bump playwright from 1.61.1 to 1.62.0 in /e2e-tests by @dependabot[bot] in #2836
- chore(deps): bump werift from 0.23.0 to 0.24.1 in /e2e-tests by @dependabot[bot] in #2837
- chore(deps): bump @astrojs/starlight from 0.41.3 to 0.41.4 in the astro group by @dependabot[bot] in #2838
- chore(deps): bump @tanstack/react-query from 5.101.3 to 5.101.4 in the tanstack group by @dependabot[bot] in #2844
- chore(deps): bump postcss from 8.5.20 to 8.5.23 in the tailwind group by @dependabot[bot] in #2845
- chore(deps): bump eslint from 10.7.0 to 10.8.0 in the linting group by @dependabot[bot] in #2847
- chore(deps): bump @tiptap/extension-underline from 3.28.0 to 3.29.1 by @dependabot[bot] in #2848
- fix(portal): post accept-invite to the portal-scoped route (#2824) by @bdunncompany in #2825
- fix(intents): seal AI-minted temporary passwords across both execution paths by @ToddHebebrand in #2853
- chore(deps): bump the hono group with 2 updates by @dependabot[bot] in #2846
- fix(deps): dedup pnpm-lock.yaml corrupted by parallel admin-merges by @ToddHebebrand in #2859
- fix(agent): raise auth-dead backoff cap above the heartbeat interval (#2792) by @ToddHebebrand in #2793
- feat(agent/backup): give the backup helper real logs by @ToddHebebrand in #2801
- fix(compose): drop remaining stale tailwind.config.mjs bind mounts (#2012) by @ToddHebebrand in #2802
- feat(extension-testkit): accept Bearer/API-key header auth in probeStockHost (#2656) by @ToddHebebrand in #2803
- fix(agent): winget scans with unreadable output report ErrScanSkipped (#2726) by @ToddHebebrand in #2804
- fix(api): clear superseded token suspension on offboarding drain entry (#2785) by @ToddHebebrand in #2808
- fix(api): map object-storage upload failures to actionable 502/503 (#2794) by @ToddHebebrand in #2810
- fix(web): shorten search trigger label + h1→h2 heading order in AI drawer (#2381) by @ToddHebebrand in #2806
- fix(web): key the toast emitter off globalThis so island module duplication can't swallow toasts (#2014) by @ToddHebebrand in #2807
- fix(web): i18n interpolation for adjacent-JSX glued strings (#2380) by @ToddHebebrand in #2809
- fix(ai): register the vulnerability tools with the chat MCP server + claim CVE vocabulary (#2605) by @ToddHebebrand in #2812
- fix(settings): stop a partner-locked field 403ing every org defaults save (#2752) by @ToddHebebrand in #2811
- fix(ci): govulncheck allowlist for GO-2026-5051 + repair Type Check on main by @ToddHebebrand in #2861
- ci(security): fail CI on stale pnpm.overrides pins (#2716) by @ToddHebebrand in #2805
- fix(ci): repair smoke-binary-source workflows by @ToddHebebrand in #2864
- chore(deps): bump vite from 8.0.16 to 8.1.5 by @dependabot[bot] in #2849
- feat(billing): normalize the invoice editor onto the quote save grammar by @ToddHebebrand in #2829
- chore(deps): bump the react group across 1 directory with 2 updates by @dependabot[bot] in #2839
- fix(web): hide Linked Profiles tab on unlinked devices (#2865) by @ToddHebebrand in #2867
- fix(web): org defaults device-group select rendered raw i18n keys by @ToddHebebrand in #2869
- fix(security): mTLS device identity, durable renewal, and edge transport trust (security wave 5) by @ToddHebebrand in #2872
- fix(ai): require durable tier-3 approval for plan steps in chat by @ToddHebebrand in #2873
- feat(m365): customer-graph-actions consent & onboarding by @ToddHebebrand in #2880
- ci(m365): wire the graph-actions executor into CI, Trivy, Dependabot, release by @ToddHebebrand in #2893
- fix(portal): prevent pre-hydration GET submit from leaking the password in the URL (#2868) by @ToddHebebrand in #2884
- fix(agent): detached self-uninstall teardown — agent no longer kills its own service mid-sequence (#2878) by @ToddHebebrand in #2886
- fix(api): make suspended→offboarding reachable via the org lifecycle API (#2879) by @ToddHebebrand in #2887
- fix(billing): durable quote response-capability writer + replay backstop (#2875) by @ToddHebebrand in #2888
- fix(reports): honor '*' wildcard grants in report authority checks (#2874) by @ToddHebebrand in #2885
- fix(remote-terminal): serialize terminal command execution and de-collide command ids (#2870) by @ToddHebebrand in #2889
- fix(remote): terminal sessions die at 60s (desktop orphan recovery sweeping non-desktop sessions) + silent UI freeze (#2871) by @ToddHebebrand in #2892
- fix(api): offboarding entry self-deadlock — reuse the request transaction (#2877) by @ToddHebebrand in #2890
- security(m365): actions signing-secret runtime smoke + hardening-guard parity by @ToddHebebrand in #2910
- fix(api): three Sentry issues — authenticator FK 500s (BREEZE-12/13), S3 endpoint save gaps (BREEZE-P), CAS 0-row diagnostics (BREEZE-X) by @ToddHebebrand in #2912
- feat(auth): add principal-kind discriminator to AuthContext by @ToddHebebrand in #2915
- fix(api): normalize malformed S3 endpoints in backup_configs (BREEZE-P backfill) by @ToddHebebrand in #2916
- feat(intents): durable-release-only guard + persisted origin principal by @ToddHebebrand in #2917
- chore(deps): bump docker/login-action from 4.4.0 to 4.6.0 in the github-actions group across 1 directory by @dependabot[bot] in #2900
- chore(deps): bump actions/github-script from 8.0.0 to 9.0.0 by @dependabot[bot] in #2901
- chore(deps): bump google.golang.org/api from 0.289.0 to 0.290.0 in /agent by @dependabot[bot] in #2882
- chore(deps): bump github.com/pion/webrtc/v4 from 4.2.17 to 4.2.18 in /agent by @dependabot[bot] in #2883
- chore(deps): bump the aws-sdk group across 1 directory with 5 updates by @dependabot[bot] in #2881
- chore(deps): bump @types/node from 26.1.1 to 26.1.2 in /e2e-tests by @dependabot[bot] in #2898
- chore(deps): bump @sentry/node from 10.54.0 to 10.68.0 by @dependabot[bot] in #2908
- chore(deps): bump @typescript-eslint/parser from 8.64.0 to 8.65.0 by @dependabot[bot] in #2904
- chore(deps): bump @radix-ui/react-toast from 1.2.19 to 1.2.23 by @dependabot[bot] in #2905
- chore(deps): bump postcss from 8.5.23 to 8.5.24 in the tailwind group by @dependabot[bot] in #2903
- chore(deps): bump the react-native-animation group across 1 directory with 2 updates by @dependabot[bot] in #2834
- chore(deps): bump js-yaml from 4.3.0 to 5.2.2 by @dependabot[bot] in #2907
- chore(deps): bump react-native from 0.86.0 to 0.86.2 in the mobile group across 1 directory by @dependabot[bot] in #2899
- chore(deps): bump the astro group across 1 directory with 5 updates by @dependabot[bot] in #2902
- chore(deps): bump jsdom from 29.1.1 to 30.0.1 by @dependabot[bot] in #2906
- feat(shared): move the intent canonicalizer to @breeze/shared with frozen conformance vectors by @ToddHebebrand in #2921
- feat(shared): comms action catalog, effect envelope, and Graph operation plan by @ToddHebebrand in #2922
- feat(shared): trim communications-delegated to mail-only scopes (v2) by @ToddHebebrand in #2924
- feat(api): delegated-connection schema — constraints, consent sessions, RLS by @ToddHebebrand in #2926
- test(api): behavioural cross-user RLS proof for comms-delegated rows by @ToddHebebrand in #2928
- docs: record the tenant-export registration list and the stacked-PR CI gap by @ToddHebebrand in #2931
- fix(security): agent updater, outbound network policy, and manifest trust (security wave 6) by @ToddHebebrand in #2933
- docs(m365): communications-delegated executor design (v3) + master-spec §6.1 amendment by @ToddHebebrand in #2920
- docs(claude): working-style guidance + accuracy fixes for CLAUDE.md by @ToddHebebrand in #2929
- docs(plans): M365 comms plan 2 — executor service (tasks 8-11) by @ToddHebebrand in #2935
- docs(plans): M365 comms plan 1 — API binding & runtime config (tasks 6-7) by @ToddHebebrand in #2934
- docs(plans): M365 comms plan 3 — API integration, consent, tools & release (tasks 12-18) by @ToddHebebrand in #2939
- feat(agent): RDS per-session helpers — session targeting, on-demand leases, per-session consent by @ToddHebebrand in #2911
- feat(agent): log which session's helper receives a consent prompt by @ToddHebebrand in #2941
- feat(m365): comms plan 2 — communications executor service (tasks 8-11) by @ToddHebebrand in #2943
- fix(api): add retention for device_metrics and service_process_check_results (#2827) by @bdunncompany in #2891
- chore(deps): bump @types/node from 25.9.3 to 26.1.2 by @dependabot[bot] in #2952
- chore(deps): bump @radix-ui/react-context-menu from 2.3.3 to 2.3.7 by @dependabot[bot] in #2953
- chore(deps): bump bullmq from 5.79.2 to 5.81.2 by @dependabot[bot] in #2954
- chore(deps): bump google-auth-library from 10.6.2 to 10.9.1 by @dependabot[bot] in #2955
- chore(deps): bump ssh2-sftp-client from 11.0.0 to 12.1.1 by @dependabot[bot] in #2909
- fix(ci): bump Node pin to 22.23.2 to unblock Cloudflare Pages by @ToddHebebrand in #2937
- ci: resolve Node from .node-version instead of a floating '22' by @ToddHebebrand in #2938
- chore(deps): bump nanoid from 5.1.15 to 6.0.0 by @dependabot[bot] in #2956
- fix(web): make Device Groups reachable and crash-proof; asList envelope guard; Compare bulk action by @ToddHebebrand in #2944
- feat(alerts): consolidate alert-rule ownership under the Alerts config-policy feature by @ToddHebebrand in #2946
- feat(m365): comms plan 1 — intent binding + runtime config (tasks 6-7) by @ToddHebebrand in #2940
- chore(deps): bump @radix-ui/react-tooltip from 1.2.12 to 1.2.16 by @dependabot[bot] in #2958
- fix(api): 404 instead of 500 on malformed uuid ids in webhook routes and cursors by @ToddHebebrand in #2918
- feat(api): cancel a partner's subscription when suspended for abuse by @ToddHebebrand in #2927
- fix(agent): keep disk inventory when one volume warns on Windows by @bdunncompany in #2945
- chore(deps): bump argon2 from 0.44.0 to 0.45.1 by @dependabot[bot] in #2960
- fix(auth): restore the payment CTA for pending partners + stop trusting payment_method_attached_at by @ToddHebebrand in #2932
- chore(deps): bump resend from 6.12.4 to 6.18.0 by @dependabot[bot] in #2959
- chore(deps): bump stripe from 22.0.2 to 22.3.2 by @dependabot[bot] in #2957
- Software deployment visibility: fix the status pipeline, add a Deployments tab, carry bulk selection into the wizard (#2866) by @ToddHebebrand in #2876
- chore(deps): bump @anthropic-ai/sdk from 0.111.0 to 0.115.0 by @dependabot[bot] in #2961
- chore(deps): bump happy-dom from 20.11.0 to 20.11.1 by @dependabot[bot] in #2962
- chore(deps): bump playwright from 1.61.0 to 1.62.0 by @dependabot[bot] in #2963
- chore(deps): bump posthog-react-native from 4.45.16 to 4.61.0 by @dependabot[bot] in #2964
- chore(deps): bump @aws-sdk/s3-request-presigner from 3.1030.0 to 3.1096.0 by @dependabot[bot] in #2965
- chore(deps): bump @tiptap/react from 3.28.0 to 3.29.1 by @dependabot[bot] in #2966
- fix(alerts): post-review fixes for the alert-rule ownership consolidation (#2946 follow-up) by @ToddHebebrand in #2969
Full Changelog: v0.102.0...v0.103.0