Breeze RMM v0.116.0 — accept quotes on a customer's behalf, a partner-scope foundation for business reports, network topology becomes an opt-in partner module, and a broad fix + security-hardening sweep across billing, AI sessions, and disk cleanup.
Summary
A tech can now accept a quote for a customer over the phone or in person — record the method and signer, attach evidence, and the invoice issues automatically — closing a workflow gap that previously required the customer to click through the portal (#6596, #6657, #6659, #6660, #6663). Business Reports gets its W01 partner-scope foundation: reports can now be owned by a partner instead of only an organization, laying the groundwork for partner-wide reporting; generating a partner-owned report still refuses until W02 ships (#6673). Network Topology (shipped v0.115.0 but unreachable) is now a real Partner Settings → Modules toggle instead of a permanently-off developer code (#6540). AI sessions get two enforcement fixes — a configured max-turns budget now actually applies (#6562), and the /workspace AI Assistant page is now hidden and blocked for roles without ai_sessions:use, matching the API gate added in v0.115.0 (#6559). Retired labour-pricing fields are now rejected outright instead of silently dropped (#6477). Disk Cleanup v2's OS-native cleaners now require an agent on 0.116.0, not just 0.115.0, after lab-found hang/miscount fixes (#6643). A wide fix sweep covers billing/time-entry edge cases, backup verification, monitor previews, and stale-write races, plus a five-item security-hardening pass.
Added
- Upgrade preflight + version history — on boot the API logs which deprecations and removals this upgrade crosses (from a shipped breaking-change manifest) and records each version it runs in
breeze_version_history. It reports only and never blocks boot; an operator CLI with--strict(orBREEZE_UPGRADE_PREFLIGHT_STRICT=true) exits non-zero when a removal is crossed (#6714). - Accept a quote on behalf of a customer (#6596): from the quote detail page, record a verbal/in-person acceptance (method + signer), and the invoice is issued immediately — gated on a new
quotes:acceptpermission, back-filled to every role that already had "send" (#6596). Optional customer-notice auto-email on acceptance, configurable in Partner Billing Settings (#6657). Evidence file can be attached to the acceptance record (#6663). A matching "Decline on behalf" action records a customer decline with an optional reason (#6659). - Business Reports partner-scope foundation, W01 (#6673, feature #3198): the
reportstable can now be owned by a partner (org_idXORpartner_id), with dual-axis RLS and no partner-wide read branch — a partner-owned report is private to that partner. No new user-visible report type is generatable yet; creating one returnsunsupported_report_scopeuntil W02. - Business Reports, W02 (#6739, feature #3198): three new report types — SLA attainment, time & billability, and AR aging — generate at org scope or partner-wide (suspended orgs excluded), on demand or on a schedule, as PDF or CSV. Billable value is priced per row in its own currency, never summed across currencies. Report-list labels and editing for these types follow in W03.
- Network Topology partner module toggle: Settings ▸ Partner ▸ Modules now has a real "Network Topology (Beta)" switch (plus physical/interface-health/diagnostics/AI sub-toggles); the device Topology tab shows a proper empty state with a link to enable it instead of a raw
materialization_disabledcode (#6540).
Improved
- Metric anomaly detection groups repeated anomalies into episodes that auto-resolve when the metric clears, with baselines protected from anomalous data. Still behind
ml.anomalies.enabled(default off); with it off, open episodes close asdetection_off(#6702, #6708). The anomaly evaluation endpoint now excludes cleared anomalies and reports episode-level figures (#6726). - Ticket Time & Billing entries with no configured hourly rate now show a clear "no rate" badge instead of inflating billable hours, and the billables list and invoice assembly agree on what counts as missing (#6581, #6583).
- Invoice line descriptions no longer bake in a "(billed X of Y worked)" note (#6587); invoice delete-preview totals round correctly at the currency's minor unit (#6612).
- DR plan editor and Backup config forms auto-scroll to the first invalid field on a failed save (#6578); DR plan save and BMR-token creation now surface success/error toasts instead of failing silently (#6580).
- Devices list correctly reflects "Offline" immediately on a WebSocket close/error instead of getting stuck "Online" (#6566).
- Configuration Policies ▸ Monitors: monitor preview works again (#6615), deleting a monitor with existing alert history no longer fails (#6556), a deleted monitor no longer reappears after reopening the policy (#6564), and a new HTTP-check expecting a 3xx status defaults "follow redirects" off (#6558).
- MCP tenant clients now see an accurate, shorter tool list instead of tools that
tools/callalways denies (#6479).
Fixed
- Ticketing: an inbound customer reply now reopens a resolved ticket through the normal status-change path (#6711), and resolve/reopen emit ML feedback events (#6715). M365 mailbox tickets take their full text from the HTML body instead of the 255-char preview (#6703) and import email attachments onto the ticket (#6709).
- Agent reliability scoring no longer counts non-hardware log lines as memory/disk signals (#6713).
- Agent disconnects no longer publish
device.offlinetwice, so webhooks and offline automations fire once (#6722). - DNS security: content-policy blocks (streaming, social media, gambling, adult content, unknown) no longer raise a high-severity "DNS threat blocked" alert per device every hour — only real threat categories alert (#6704). Provider labels like "Adult Themes", "Trading" or "Download" are no longer misfiled as adware (#6706).
- Windows event logs: System-log errors are now classified as hardware (disk, NTFS, storage, WHEA, thermal) or system instead of one bucket (#6705).
- Several stale-write races in billing now refuse instead of silently succeeding: editing/deleting an already-changed time entry, ticket part, or time-entry row now errors instead of reporting a false success (#6567, #6588, #6613, #6617).
- Ticket time entries hitting the
billable_minutesdatabase constraint now show a friendly refusal instead of a 500 (#6616). - Backup verification tab shows the agent's actual failure reason instead of a generic message (#6618).
/devices/networkno longer renders a broken device-detail shell when navigated to directly (#6560).- Bare-metal BMR token minting is refused up front when the source snapshot isn't restorable, instead of minting a token that fails later (#6548).
- SSO login failures now return a proper "Invalid credentials" error instead of a generic 500 (#6623).
- Microsoft mailbox "Connect" no longer 500s on an admin-consent denial; shows a friendly error instead (#6553). Inbound email-to-ticket gains flood backpressure (global rate ceiling, default 20 jobs/sec), loop/bounce suppression, and an opt-in full-message-reply mode (#6534).
- AI-session turn limits configured in Settings ▸ AI Usage now actually end a session at the configured max, instead of every session silently defaulting to 50 turns regardless of configuration (#6562).
- Retired labour-pricing fields (
defaultHourlyRate,defaultBillable,rateCurrency) are now rejected with a 400 naming the field and its billing-profile replacement, instead of being silently accepted and dropped (#6477). - Disk Cleanup: freed-space totals are now correct on lazy-reclaim filesystems (#6546) and on btrfs hosts with bind mounts (#6545); the retired Windows
cleanmgrsub-ID is reachable again in selection (#6606); cleanup run history renders correctly (#6602).
Security
/workspace(AI Assistant full-page chat) and its sidebar nav entry are now gated onai_sessions:use, closing a gap where a role without the permission could open the composer and only discover it was denied on send (#6559).- IP allowlist is now checked before a request transaction opens, avoiding unnecessary DB work on a request that will be denied (#6532).
- A failed passkey verification no longer echoes the expected WebAuthn origin string in the error response (#6565).
- Tier-2 AI-tool release authority, admin-history bounds, and contract-tool/line-materialization caller-authority checks are tightened so an over-privileged AI session or contract action is denied at the authority boundary rather than allowed through (#5570).
Self-Hosting / Upgrade Notes
Upgrade: bump BREEZE_VERSION in .env, then docker compose pull api web portal && docker compose up -d (or pnpm install when running from source).
Migrations: 22 new migration files, all idempotent and auto-applying on boot via autoMigrate. Read through them; none does a large-table rewrite or backfill against existing hot data — they add new tables/columns for quote acceptances, backup-provider integration, AI Operator task graph, caller verification, metric anomaly episodes (plus an episode feedback source type), the reports partner-ownership shape, and breeze_version_history (the app role gets SELECT only). No action required beyond the normal upgrade.
No new required environment variables. New optional env vars, both defaulted/off:
INBOUND_QUEUE_MAX_PER_SEC(default20) — global inbound-email-to-ticket processing rate ceiling; jobs are delayed, never dropped, when over the ceiling.CALLER_VERIFICATION_ENABLED— must be the exact literaltrueto enable; defaultfalse/unset keeps every/caller-verificationroute 404. Backend-only in this release, no reachable UI.
Behavior changes & flags:
- Disk Cleanup v2's OS-native cleaners now require agent 0.116.0, not 0.115.0 — an agent still on 0.115.x gets a clear "update the agent" refusal instead of running the pre-fix cleaners that could hang or misreport (#6643).
quotes:acceptis a new permission, back-filled onto every role that could already send a quote, gating the new accept/decline-on-behalf actions (#6596).- Retired labour-pricing fields (
defaultHourlyRate,defaultBillable,rateCurrency) now return a 400 instead of a silent 200 no-op — this is the release where the rejection actually takes effect; v0.115.0 still accepted and silently discarded these writes (#6477, #6639). - Confirming an account's email now requires clicking "Confirm my email" on the verify-email page instead of auto-verifying on page load (#6579).
/workspaceand the "AI Assistant" nav item are now hidden/blocked for any role lackingai_sessions:use(#6559); AI session turn limits are now actually enforced at the configured value (#6562).- Network Topology is now a real Partner Settings ▸ Modules toggle (
settings.topologyFeatureFlags) — still defaults off; existing partners see no change until they opt in (#6540).
No breaking changes.
Known gaps in this release:
- Customer portal invoice view (logged in) shows invoice lines grouped by ticket but without the ticket category label; the web invoice, the PDF and the public invoice link are correct (#6674).
- Windows System-log hardware/system classification (#6705) passed its collector test suite natively on Windows Server 2022, but live event-log collection was not exercised end to end before release.
What's Changed
- docs: v0.115.0 release docs sweep by @ToddHebebrand in #6513
- chore(deps): bump zustand from 5.0.14 to 5.0.15 by @dependabot[bot] in #6528
- fix(mcp): stop advertising tenant tier-3 tools that tools/call always denies (#6401) by @ToddHebebrand in #6479
- fix(billing): reject the retired labour-pricing fields instead of silently dropping them (#6472) by @ToddHebebrand in #6477
- docs(ai): A-W05 output-efficiency wave plan + index decisions D10–D16 by @ToddHebebrand in #6478
- Harden inbound email-to-ticket: global flood backpressure, loop/bounce suppression, opt-in full-message replies by @bdunncompany in #6534
- fix(api): use BullMQ-safe SentinelOne sync job IDs by @hcaldicott in #6530
- fix(api): use BullMQ-safe config-policy run IDs by @hcaldicott in #6531
- feat(config-policy): role and OS filters on partner-owned assignments by @advenimus in #6235
- fix(ci): scope mobile-native detection to the merge-queue entry's own base by @ToddHebebrand in #6471
- chore(deps): bump @elastic/elasticsearch from 9.5.0 to 9.5.1 by @dependabot[bot] in #6527
- fix(auth): check IP allowlist before request transaction by @hcaldicott in #6532
- docs(security): YARA IOC scanning — W01 live-policy plan + index (#6263) by @ToddHebebrand in #6543
- docs(security): caller verification (anti-vishing) spec v5 + W01–W05 plans (#6354); service deliverables plans by @ToddHebebrand in #6544
- test(agent): deflake TestRunBrewCleanupBoundedCancelsDescendants under -race by @ToddHebebrand in #6550
- test(api): wait for the fire-and-forget move-org audit instead of racing it by @ToddHebebrand in #6554
- feat(ai-tools): A-W03 description diet — one description surface, 300/160 budget, zero baseline by @ToddHebebrand in #6468
- fix(monitors): conversion pre-transaction reads take the caller's DB context (regression from #6416) by @ToddHebebrand in #6480
- feat(web): settings consolidation W02 — shared InheritedField, SLA direction notes, InboundEmailCard split by @ToddHebebrand in #6542
- fix(agent): dedupe btrfs bind mounts by device in cleanup volume scan (#6483) by @ToddHebebrand in #6545
- fix(backup): refuse bare_metal BMR token mint when snapshot isn't restorable (#6470) by @ToddHebebrand in #6548
- fix(m365): complete Microsoft consent callback flow by @hcaldicott in #6533
- feat(ai-operator): recipe registry and coordinator dispatch refactor (E1) by @ToddHebebrand in #6551
- fix(monitors): default followRedirects off for a 3xx http_check expectation (#6510) by @ToddHebebrand in #6558
- fix(web): gate /workspace and the AI Assistant nav on ai_sessions:use (#6498) by @ToddHebebrand in #6559
- fix(api): apply configured maxTurnsPerSession to new AI sessions (#6473) by @ToddHebebrand in #6562
- fix(config-policy): stop resurrecting deleted monitors on the Monitors tab (#6493) by @ToddHebebrand in #6564
- fix(api): guard persistDrAuthorizationDenial with a compare-and-swap (#6457) by @ToddHebebrand in #6563
- fix(api): refuse a zero-row updateTimeEntry write instead of echoing the stale row (#6462) by @ToddHebebrand in #6567
- fix(ai): revalidate tier-2 release authority, bound admin history reads, bind contract tools and line materialization to caller authority (SEC-050, SEC-141, SEC-144, SEC-145) by @ToddHebebrand in #5570
- chore(deps): bump eslint from 10.9.1 to 10.10.0 in the linting group by @dependabot[bot] in #6525
- fix(agent): settle free-space reading before computing freedBytes on lazy-reclaim filesystems (#6484) by @ToddHebebrand in #6546
- feat(integrations): backup provider foundation — schema, Cove adapter, connection routes (#6008 W01) by @ToddHebebrand in #6552
- fix(web): show the reason for a plain server-refused AI tool call (#6500) by @ToddHebebrand in #6557
- fix(web): stop /devices/network from rendering a phantom device page (#6501) by @ToddHebebrand in #6560
- fix(api): DELETE /monitor-definitions/:id succeeds once alerts exist (#6509) by @ToddHebebrand in #6556
- fix(api): never echo the WebAuthn expected origin on a passkey verification failure (#6499) by @ToddHebebrand in #6565
- fix(backup): canonicalize S3 accessKeyId/secretAccessKey to accessKey/secretKey (#6511) by @ToddHebebrand in #6569
- chore(deps): bump @testing-library/dom from 10.4.1 to 10.4.2 in the testing group by @dependabot[bot] in #6524
- fix(web): show a no-rate indicator instead of inflated billable hours (#6466) by @ToddHebebrand in #6581
- fix(api): listBillables and invoiceAssembly agree on missing-rate gap semantics (#6461) by @ToddHebebrand in #6583
- fix(billing): stop baking the worked-vs-billed note into invoice descriptions (#6467) by @ToddHebebrand in #6587
- fix(api): refuse a zero-row updateTicketPart write instead of echoing the stale part (#6568) by @ToddHebebrand in #6588
- fix(api): persist offline transitions from the WS close/error handlers (#6503) by @ToddHebebrand in #6566
- fix(web): scroll validation errors into view on Backup/DR forms (#6494) by @ToddHebebrand in #6578
- chore(deps): bump hono from 4.13.5 to 4.13.8 in the hono group across 1 directory by @dependabot[bot] in #6523
- docs(reports): PSA business reports — spec refresh + W01–W03 implementation plans (#3198) by @ToddHebebrand in #6582
- feat(api): AI Operator task targets, accounts, steps and events (recipe library E2) (#6167) by @ToddHebebrand in #6595
- feat(portal): Customer Portal Network Visibility read model + flag (PR 1 of #5861) by @fabicarvano in #6175
- chore(deps): bump autoprefixer from 10.5.4 to 10.6.1 in the tailwind group by @dependabot[bot] in #6522
- fix(web): require a click to confirm email verification (#6539) by @bdunncompany in #6579
- fix(web): DR plan save and BMR token create surface via runAction (#6495) by @ToddHebebrand in #6580
- chore(deps): bump @tanstack/react-query from 5.102.8 to 5.103.1 in the tanstack group by @dependabot[bot] in #6521
- chore(deps): bump the react group across 1 directory with 4 updates by @dependabot[bot] in #6520
- fix(agent): end cleanmgr's session-0 hang and stop claiming its handlers completed (#6482) by @ToddHebebrand in #6547
- feat(backup-providers): W02 sync — job, persistence, mapping, matching, alerts (#6010) by @ToddHebebrand in #6591
- chore(deps): bump the astro group across 1 directory with 4 updates by @dependabot[bot] in #6519
- fix(ci): stand down the rate limiters that red the portal dev E2E job (#6447) by @ToddHebebrand in #6611
- fix(web): round the invoice delete preview at the currency's minor unit (#6441) by @ToddHebebrand in #6612
- fix(api): refuse a zero-row deleteTicketPart instead of reporting success (#6589) by @ToddHebebrand in #6613
- fix(api): guard the agentWs onOpen markOnline await (#6607) by @ToddHebebrand in #6614
- fix(monitors): restore preview auth via the auth module's canonical closures (#6445) by @ToddHebebrand in #6615
- fix(api): only re-derive billed minutes when the terms actually change (#6465) by @ToddHebebrand in #6617
- fix(backup): persist the agent's verification error as the failure reason (#6561) by @ToddHebebrand in #6618
- test(api): wait for fire-and-forget move-org audit rows instead of sleeping (#6555) by @ToddHebebrand in #6619
- fix(e2e): log in once per Playwright worker so each owns its refresh-token family (#6447) by @ToddHebebrand in #6625
- fix(api): map the billable_minutes CHECK violation to a typed refusal (#6463) by @ToddHebebrand in #6616
- chore(deps): typescript-tooling 5.9.3 + perf_hooks histogram type fix by @ToddHebebrand in #6609
- fix(agent): make retired cleanmgr sub-id reachable from selectionFor by @ToddHebebrand in #6606
- fix(disk-cleanup): system cleanup paper cuts from W05 lab run (#6485) by @ToddHebebrand in #6602
- feat(ai-operator): human-work steps bound to ticket checklists, and the maintenance_window wait writer (E3) (#6168) by @ToddHebebrand in #6600
- feat(security): live IOC scan scheduler, Security tab cleanup, Scans page, neutralized quarantine (#6263 W01) by @ToddHebebrand in #6573
- feat(topology): partner Modules toggle for Network Topology + real empty state on the device tab by @ToddHebebrand in #6540
- fix(tickets): mailbox Connect 500 + Microsoft admin-consent error redirect handled by @ToddHebebrand in #6553
- feat(caller-verification): backend core, flag off (W01 #6354) by @ToddHebebrand in #6576
- chore(deps): bump resend from 6.18.0 to 6.28.1 by @dependabot[bot] in #6529
- fix(api): emit INVALID_CREDENTIALS from genericAuthError so sso login… by @fabicarvano in #6623
- chore(deps): bump the go-minor-patch group in /agent with 2 updates by @dependabot[bot] in #6624
- chore(deps): batch dependabot bumps (#6601 #6541 #6517 #6515 #6514) by @ToddHebebrand in #6608
- docs(ai): Tool Catalog W02 — OpenAPI source kind plan (#5215) by @ToddHebebrand in #6549
- docs(deploy): prune stale images and build cache after each droplet deploy by @ToddHebebrand in #6628
- chore(deps): bump the docker-base-images group across 6 directories with 1 update by @dependabot[bot] in #6536
- feat(quotes): accept a quote on behalf of a customer and issue the invoice by @ToddHebebrand in #6596
- fix(agent): give pwsh cold-start headroom in paramrefs test (#6599) by @ToddHebebrand in #6647
- fix(billing): the retired-pricing rejection ships in v0.116, not v0.115 (#6472) by @ToddHebebrand in #6639
- fix(quotes): write accept-on-behalf audit before post-commit side effects by @ToddHebebrand in #6658
- fix(billing): accept-on-behalf dialog reads auto-email flag from quote detail (#6636) by @ToddHebebrand in #6656
- fix(web): friendly copy for send-gate refusals in the accept-on-behalf dialog (#6637) by @ToddHebebrand in #6660
- feat(quotes): decline a quote on behalf of a customer (#6634) by @ToddHebebrand in #6659
- feat(quotes): attach an evidence file to an accept-on-behalf record (#6633) by @ToddHebebrand in #6663
- feat(quotes): optional customer notice when a provider records their acceptance (#6635) by @ToddHebebrand in #6657
- feat(bare-metal): agent follows cross-snapshot references under snapshot-file-membership-v1; refuse before provision (W09b) by @ToddHebebrand in #6491
- feat(portal): Network overview page and nav entry (#6640) by @ToddHebebrand in #6646
- docs: metric anomaly episodes spec + W01–W04 plans (#6650) by @ToddHebebrand in #6655
- fix(backup): retentionDays is a floor GFS keepDaily can't shorten (#5400) by @ToddHebebrand in #6661
- fix(agent): make heartbeat watchdog + scheduleCleanup tests deterministic (#6645) by @ToddHebebrand in #6662
- fix(db): winget sync holds no tx across GitHub; wedge scanner reclaims (#6348) by @ToddHebebrand in #6642
- fix(api): gate system cleanup on agent 0.116.0 (W06 fixes) by @ToddHebebrand in #6643
- fix(device-groups): clean up config policy assignments before deleting device group by @mvthul in #5855
- fix(ticket-mailbox): fall back to live Graph role check when wids is absent Fixes #5487 by @JayDawgZA in #5489
- feat(billing): group invoice lines by ticket and category across PDF, portal, and web by @mvthul in #5856
- fix(topology): resolve heartbeat topology flags before the org transaction (US pool deadlock) by @ToddHebebrand in #6671
- docs: metric anomaly episodes — second quorum amendments (#6650) by @ToddHebebrand in #6681
- feat(anomalies): metric anomaly episodes W01a — table, migration, registrations, shared types by @ToddHebebrand in #6678
- fix(topology): resolve write flags before taking the site-state row lock by @ToddHebebrand in #6679
- fix(partner-export): child UPDATE triggers take the org lock only on material change by @ToddHebebrand in #6682
- fix(web): v0.115.0→main pre-release sweep — 4 fixes + tracking doc by @ToddHebebrand in #6686
- feat(reports): W01 partner-scope reports foundation (#3198) by @ToddHebebrand in #6673
- fix(dns): raise DNS threat alerts only for threat categories (#6692) by @ToddHebebrand in #6704
- fix(dns): check adult_content before adware in DNS category mapper (#6693) by @ToddHebebrand in #6706
- fix(agent): classify Windows System-log errors as hardware or system (#6694) by @ToddHebebrand in #6705
- feat(web): What's New entry for 0.116.0 by @ToddHebebrand in #6712
- feat(anomalies): metric anomaly episodes W01c — baseline anti-contamination with fallback by @ToddHebebrand in #6702
- fix(agent): anchor reliability memory/disk signals to hardware phrasing (#6696) by @ToddHebebrand in #6713
- fix(ticket-mailbox): derive full text from HTML Graph bodies instead of bodyPreview (#6687) by @ToddHebebrand in #6703
- fix(ticket-mailbox): import M365 email attachments onto tickets (#6688) by @ToddHebebrand in #6709
- fix(ticketing): reopen resolved tickets on inbound reply via changeTicketStatus (#6689) by @ToddHebebrand in #6711
- fix(ticketing): emit ticket.resolved/reopened ML feedback on status change (#6697) by @ToddHebebrand in #6715
- feat(anomalies): metric anomaly episodes W01b — assembly, auto-resolve, stage wiring by @ToddHebebrand in #6708
- feat(api): upgrade preflight — breaking-change manifest, boot report, version history (#6605) by @ToddHebebrand in #6714
- fix(test): replayMigration tracks constraint rewrites (#6700, #6701) by @ToddHebebrand in #6717
- docs(qa): v0.116.0 final sweep at release cut by @ToddHebebrand in #6723
- fix(api): publish device.offline once on agent disconnect (#6566 regression) by @ToddHebebrand in #6722
- docs(bare-metal): W06 Windows rebuild engine plan (#5499) by @ToddHebebrand in #6727
- docs(qa): v0.116.0 delta sweep at tag commit by @ToddHebebrand in #6728
- feat(analytics): anomaly episode evaluation — feedback source type, episodes block, cleared exclusion (W03) by @ToddHebebrand in #6726
- docs: sync technical docs for v0.116.0 by @ToddHebebrand in #6730
- feat(reports): business reports W02 — registry map, SLA / time & billability / AR aging generators, partner-scope generation (#3198) by @ToddHebebrand in #6739
- fix(release): trust SSL.com's new TLS root in CodeSignTool's JDK by @ToddHebebrand in #6742
New Contributors
- @JayDawgZA made their first contribution in #5489
Full Changelog: v0.115.0...v0.116.0