Repository navigation
Read before upgrading
0.15.0 supports fresh deployments only. There is no in-place upgrade from 0.14.x or earlier, and no return to them. Changing YUZU_VERSION on an existing install is not a supported upgrade. Deploy into a new database. Details: Upgrading, Deployment contract.
If you run a gateway
- The gateway image and its
sys.configare an atomic pair. Listeners move from{grpcbox, [{servers, ...}]}to{yuzu_gw, [{grpc_listeners, ...}]}, and the upstream channel from{grpcbox, [{client, ...}]}to{yuzu_gw, [{upstream_client, ...}]}. Convert both in one edit. The shipped configs and compose files are already converted; a config you wrote is not. A gateway with an old-shape block refuses to boot. An old gateway image given a new-shape config binds no listener, logs nothing, and still answers/healthzand/readyzwith 200, so roll the image and config back together too. - The server now refuses to start with
--gateway-upstreamunless a gateway peer is authorised. The generated default certificates are pinned automatically. With your own certificates, configure--gateway-peer-pinor--gateway-peer-pin-file(envYUZU_GATEWAY_PEER_PINS/YUZU_GATEWAY_PEER_PIN_FILE). - Heartbeats are bound to the connection that opened the session. Agents must reach the gateway directly or through L4 / TLS passthrough; HTTP/2-terminating proxies are unsupported. Gateway restart required.
- Upgrade agents first (0.15.0 or later), then the gateway. Released v0.13.0 and v0.14.0-rc6 agents wedge in their reconnect path with default settings (#2182, fixed by #5183) and recover only with
--no-auto-update. v0.12.0 agents never recover.
Access changes that return 403 where they did not before
- Stored responses of Forensics-tier plugins (
app_usage,browser_inventory,execution_artifacts,privacy_permissions,remote_access_audit) needForensics:Readas well asResponse:Read(#4990). - With RBAC off (the default), the audit trail is admin-only (#5771):
GET /api/audit,GET /api/v1/audit, MCPquery_audit_logand theyuzu://audit/recentresource. - Result-set members need
Infrastructure:Readand are limited to devices the owner can still see. With RBAC enforcement on, onlyAdministratorandITServiceOwnerhold it by default, so owners with other roles get403on their own sets until it is granted. The default RBAC-off configuration is unaffected. GET /api/config,GET /api/v1/configandget_runtime_configshow only the overrides the caller could write.- Approving or rejecting an approval request needs an interactive session and a fresh MFA step-up; MCP can no longer approve (#6061, #6079).
- MFA step-up is one central check on 37 named routes, and it now runs before the route's own authorization.
- Cookie-session requests that change state need a same-site
OriginorReferer. API and MCP token callers are unaffected. - Changing sign-in provider settings, an account's role, or elevation eligibility needs your own administrator role, not a temporary elevation.
- Enrollment tokens default to 72 hours and 100 uses when a limit is left blank.
Other breaking changes
yuzu-server.cfgno longer overrides a stored password (#5274). A password you rotated by editing the file, or by re-running the Windows installer with a new/ADMIN_PASS, reverts to the one in the auth database. If you rotated because the old one was exposed, reset it in the product straight away.- Windows installer:
/OPERATOR_USERand/OPERATOR_PASSare removed (#5343). A fresh install passing either is refused with exit code 7; an upgrade passing a non-empty/OPERATOR_PASS=with exit code 11. - TAR is renamed Echo across the agent plugin,
/api/v1/echo/..., MCP tools,yuzu_echo_*metrics,echo.*audit actions and instruction ids. Update dashboards, alerts and scripts. - OpenAPI: 262 of 264 operations now type their success response and every
operationIdis unique. Generated clients and byte-level pins change. registryplugin 2.x: registry links are refused instead of followed, so a write underHKLM\SYSTEM\CurrentControlSetnow fails; write underControlSet001instead.GET /loginis a302to/home, and unauthenticated page requests go to/home.- Source builds: the Erlang gateway needs OTP 28.4 or any later 28.x. Otherwise
meson setupskips the gateway with a warning (-Drequire_gateway=truemakes it an error). --max-agents/YUZU_MAX_AGENTSis deprecated and has no effect.
Security fixes that need action from you
- NVD HTTP proxy credentials (
user:password@) were shown in the settings panel and written to the startup log. Rotate the proxy credentials, and purge or restrict old server logs and any saved copy of that page.
Known issue
- Mixed-version fleets and
registrywrites (#6484). The server does not checkminAgentVersionat dispatch. Aregistryset_valueusing the 2.xdata_hexor the new value types, sent to an agent still on registry 1.x, writes an empty or mistyped value and reportsstatus|ok. Upgrade every agent before you use those fields.
Full notes
This page shows only part of the changelog. The complete list, including every Breaking and Security item, is in CHANGELOG.md.
Changelog (truncated)
Added
-
local_security_policy plugin: Windows leg and the
sudoersaction. Windows readspassword_policy,lockout_policyandaudit_policyfrom one read-onlysecedit /export /areas SECURITYPOLICY(argv leaf, no shell,argv[0]under the resolved system directory); the export is staged in an agent-owned, owner-only scratch directory underagent.data_dir, removed on return and swept by a later dispatch if orphaned, and an export that does not end in the signed[Version]section reportssecedit:export_incomplete, never rows.sudoers(Linux and macOS) reads/etc/sudoersand/etc/sudoers.d/*, lexed as sudo 1.9.16 lexes them, and reports one row per rule clause; an unreadable file ispermission_denied/unreadable, never an empty result, and Windows reports a fixedwindows_has_no_sudoersrow. The action isSecurityread-only with a Medium risk tier. -
privacy_permissions macOS leg. The macOS leg now reads per-app camera, microphone and full-disk-access grants from the system and every real per-user
TCC.db(location is administered outside TCC and reports an explicitunsupportedrow), read-only and in-process (never asqlite3CLI shellout), over an immutable descriptor with bounded schema/row/value/byte/time reads; it replaces the singleunsupported/macos:plannedplaceholder row released in 0.14.0. The plugin is still off until an operator enables it. An agent identity without Full Disk Access readsdeniedfor every source and reportsPERMISSION_DENIED; the production root LaunchDaemon's outcome and how to grant it Full Disk Access are not yet measured, and per-user rows report what that user's ownTCC.dbrecords, not whattccdenforces. Windows ships in the same release (see the privacy_permissions Windows leg entry). -
privacy_permissions Windows leg. Reads each real profile's
CapabilityAccessManagerConsentStore (the loaded HKU hive first, else the profile'sNTUSER.DATunderSeBackup/SeRestore) and the HKLM device toggle for camera, microphone, location and full-disk-access, the most restrictive value winning, with per-app last-used times; it replaces theunsupported/windows:plannedplaceholder row listed under 0.14.0. The offlineNTUSER.DATload is refused for a UNC, non-fixed-drive, reparse-point or reparse-ancestor, redirected, offline-placeholder, oversized or foreign-owned hive file, or one with a reparse-point or hard-linked transaction-log sidecar, and a file whose identity changes across the load is unloaded unread; a ConsentStore change reported during the read refuses that source rather than guessing. The 15 s deadline is cooperative, not a hard cap. The plugin is still off until an operator enables it; the switch is per plugin, not per OS, so a fleet that already enabled it for Linux or macOS reads Windows profiles on upgrade (dispatch is stillAdminOrApproval, single-target and audited). An offline mount alters the logged-off profile's hive sequence number and hash. -
The
filesystemplugin'sget_aclaction now reports the access-control entries on Linux and macOS, not just the basic owner, group and permission bits. Linux decodes the POSIX ACL extended attributes (system.posix_acl_accessand, for a directory,system.posix_acl_default) into one row per user, group, mask and other entry, with default entries marked. macOS reads the extended ACL and reports each entry's allow or deny, principal, permissions and inheritance flags. On Windows the descriptor's control flags are reported as acontrolrow, each entry carries its inheritance flags, and a conditional or other unmodelled entry is reported as a row of its own instead of being dropped. A read that fails is reported as unreadable with a reason, never as a file with no ACL. Windowsacerows now carry a fifth field with the inheritance flags, an undecoded ACE type prints-for its mask instead of a zero, and a failedGetAceorGetAclInformationnow ends the command with an error instead of silently returning fewer rows. -
New read-only
mgmt_postureagent plugin — which management plane a device is under, on Linux and macOS (the Windows leg is not read yet). The singlepostureaction reports facts only and changes nothing. On Linux it reads/etc/sssd/sssd.confand/etc/sssd/conf.d/*.conffor the active SSSD domains and reports the plane asad,ipaornone(an IPA realm in/etc/ipa/default.confalso reportsipa), plus whether/etc/krb5.keytabis present; it reports the configuration as written, not the live join state. On macOS it reads MDM enrolment and the MDM server host fromprofiles status -type enrollment. An SSSD configuration the agent is not allowed to read reportspermission_deniedwith planeunknown, never "not joined", and a failed or partial read reports a constrained status with a token instead of an empty result (a macOS tool that cannot be started reports unavailable instead). The Windows leg reports oneunsupportedstatus row (windows:planned) until it ships. -
network_config
routesaction. The network configuration plugin can now return the host's full routing table on Windows, Linux and macOS, IPv4 and IPv6 together: destination prefix, next hop, interface, metric, table, route type and who installed it, capped at 20,000 rows. It reads the table natively and spawns nothing (rtnetlink on Linux, the routing-socket table dump on macOS, the IP Helper forwarding table on Windows). Each OS is reported as constrained because what it hides or reports differs (the host's own addresses, cloned and local entries, a multipath route's extra next hops); the differences are documented per OS. A table that could not be read completely is reported as unavailable or incomplete, never as an empty table: on Linux a dump the kernel aborts, or flags as changed mid-read, is marked incomplete. Same securable, risk tier and approval gate as the othernetwork_configactions; the result definition isdevice.network_config.routes. -
Shared agent primitives in agents/shared: hop-by-hop O_NOFOLLOW directory open and bounded no-follow regular-file read (posix_secure_read.hpp, single-component dir-fd contract), in-memory SHA-256 (sha256.hpp, moved from license_scan) and the [redacted-email] helper (email_redaction.hpp, moved from browser_inventory); no plugin output changes (#4866).
-
Software catalogue MCP twins and raw-route pagination and exports. MCP tools
query_software_catalogueandget_software_versionsshare the REST catalogue's model.GET /api/v1/inventory/softwareandquery_installed_softwaregain aversionfilter, asourcefield per row and opaquecursor/next_cursorpagination; the REST route also gainsq,kind,ecosystemandsourcefilters and aformat=csv|jsonexport of up to 10000 rows (REST-only; a CSV export carries its continuation in anX-Next-Cursorheader and ends with a# result_truncated_by_caprecord when more rows may exist). -
Software catalogue REST routes.
GET /api/v1/inventory/software/cataloguereturns the fleet software title catalogue withtotalsthat cover every row under the active filters, first-page KPIs (headline numbers plus reporting/connected/stale device counts) and aformat=csvtitles export;GET /api/v1/inventory/software/catalogue/versionsreturns installs per version. Keyset cursors are bound to the filter set and to the hourly catalogue refresh (a stale or mismatched cursor is a400), every served page or file is audited, a degraded store (or, for the fleet export and the unfiltered versions read, a catalogue not yet built) is a retryable503, a confined caller is denied the fleet view but may read an in-scope device, and service-scoped tokens are denied. -
Server: application stability score core. A pure, transparent, uncalibrated stability score for an application or one version of it, from the share of reporting devices affected, per-device-capped crash and hang counts, and an optional version-over-version rate-ratio interval. It returns the four deductions (breadth 60, crashes 20, hangs 8, regression 12 points) alongside the 0-100 score and an excellent / good / fair / poor band (90 / 75 / 60), banded on the score rounded to one decimal so the band always matches the displayed value, and withholds the score rather than showing 0 or 100 when the population is too small or the inputs contradict each other. Not surfaced by this entry: no page, route or API reads it here, so this entry alone has no operator-visible change;
GET /api/v1/dex/appsandlist_dex_appsserve the score in the "DEX Apps list: per application and per version stability" entry of this release. -
DEX statistics core: the server gains one tested home (
dex_stats.hpp) for the statistics the DEX application views will print — Wilson intervals for proportions, exact Poisson intervals for rates over exposure, the rule of three for zero counts, an exact conditional rate-ratio interval with a minimum-exposure guard, a small-count reliability flag, and a cohort-floor helper that withholds the statistic belowkDexCohortFloordevices (one device: only an empty cohort is withheld), reporting only the device count and a below-floor flag; intervals are 95 percent two-sided except the one-sided rule of three; rates exact up to one million events with closed-form bounds beyond; rate ratios with the exact conditional interval, to within 2e-5 relative, for arms up to one billion events (none above); no page or API changes in this entry (the DEX app performance and Apps list entries in this release print these statistics). -
Instruction discovery now publishes the schema the server enforces.
discover_instructions(MCP) andGET /api/v1/discover/instructionscarryinput_schemaper definition: the canonical JSON Schema thatPOST /api/instructions/{id}/executeenforces (ornull), alongsideinput_schema_error(null,parameter_schema_not_canonicalisable, orinput_schema_budget_exceeded). Canonicalisation per request is bounded by a 4 MiB aggregate budget over stored schema text, charged in name order (ties by id), so a definition that does not fit gets the budget token instead of a schema.parameter_schemais unchanged. A non-nullinput_schemacan still be refused at execute for a reason canonicalisation does not check, such as a regex that does not compile. A new description lint,scripts/ci/check-definition-descriptions.py, ratchets the shipped definitions' descriptions, with an advisory CI jobdefinition-descriptions. -
DEX app performance: per-version crash and hang join.
GET /api/v1/dex/perf/appand the MCP toolget_dex_app_perfnow carry aversions[]array joining each version with the crash and hang observations of up to the last 30 days (clamped inside the observation retention): fleet-wide counts and affected devices (breadth), the exposure (devices with retained resource-significant samples, labelled as such), the crash-and-hang rate among exposed devices with its exact 95 percent interval, and the rate ratio against the previous canonical version with an exact interval. The join carries no device floor: counts, exposure, the rate and the ratio are published at any device count; the per-(version, day) point floor is unchanged. The route now answers503when either underlying read degrades, so a caller that reads onlypoints[]must handle it. Additive otherwise; existing fields are unchanged. -
Server: application score policy core. A pure, validated score policy that turns the band edges, tier names and deduction weights into validated policy data: the three lower tier edges and the four tier display names, the four deduction weights, and bounded per-application overrides keyed by ASCII-lowercased image name that inherit anything they do not set. A resolver returns the effective policy for an application, and the stability score's band edges are now a parameter, so the same score helper runs under a customer's policy; the score itself now refuses unusable edges (withheld as
invalid_edges) instead of banding them. Policies are refused whole with a typed reason (edges out of range, not exact at one decimal, not descending or less than a point apart; a bad name; a negative, negative-zero or non-finite weight, or weights totalling over 100; an empty, oversized, non-printable-ASCII, non-lowercase or duplicate application key; too many overrides). The shipped defaults reproduce the existing score exactly. Not surfaced by this entry: no store, route or page reads it here, so this entry alone has no operator-visible change; the stored policy and theGET,PUTandDELETE /api/v1/dex/score-policyentries of this release read it. -
New read-only
sharesagent plugin — what a host is configured to serve over SMB and NFS, on Linux (the macOS leg is not read yet). The singlelistaction reports names, paths, read-only or read-write, whether anonymous access is configured and a few flags, and never ACLs or NFS client host lists (NFS clients are a count). On Linux it reads/etc/exports,/etc/exports.d/*.exportsand/etc/samba/smb.conf. It reports the configuration as written, not livenfsdorsmbdstate, and the Samba registry backend,include =files and usershares are reported as constrained tokens instead of a wrong share list. Every source reports whether it was read, so a refused or failed read reports a constrained or permission-denied status with a token, never an empty result. Share names and paths can embed account names, so dispatch is gated at the infrastructure read permission and the stored result is readable byResponse:Readholders, as for every plugin. The macOS leg reports oneunsupportedstatus row (macos:planned) until it ships. -
Workflow run list:
GET /api/v1/workflow-executionsand MCPlist_workflow_executions. The first way to list workflow runs (only fetch-by-id existed). Newest-first keyset paging (cursor/next_cursor), filtersworkflow_id,status,since,until,limit, andlatest_per_workflow=truefor each workflow's newest visible run (the Workflows table's "last run"). Rows are metadata only (id,workflow_id,workflow_name,workflow_deleted,status,current_step,started_at,completed_at);agent_idsand step results stay on the detail route. Gated onWorkflow:Readthrough the fleet-read chokepoint: a confined caller (management-group or service-scoped) sees only runs with at least one agent in its scope, applied in the database before ordering and the page limit so hidden runs never take a page slot, shape a cursor or count towardpagination.total. Input is strict (unknown or repeated parameters are400), a store failure or the 1.5 s statement timeout is a503withretry_after_ms(never an empty list), and no audit row is written for a successful list. -
Add, remove, change and sign out accounts from the Users and roles page. The Access page now has Add user, Remove user, Change role (admin or user) and Revoke sessions, each with an in-page confirmation instead of a browser prompt. When a second-factor check is needed the page asks for the code and repeats the action once; OpenID Connect sessions are told to sign in again at their identity provider. A refusal from the server (name taken, weak password, last administrator) is shown as it came, and after a server error the page reloads so it shows what the store holds. Your own row and single sign-on accounts are never offered a control the server would refuse.
-
Access section with a Users page. The sidebar gains an Access group whose
Users entry (/access/users) lists the server's accounts (name, source, account role)
beside a panel for the selected account. It re-surfaces what Settings shows today without
adding any action in this entry: adding, removing and changing accounts stays in Settings here, and the "Add, remove, change and sign out accounts from the Users and roles page" entry in this release adds those actions to this page. An account
store that does not answer is shown as a red "could not be read" message with Retry, never
as an empty list. Opening the page as a non-administrator writes no audit row; the panels
are refused quietly. A refused API or MCP token is still recorded as an administrator-required denial. -
Role assignments, Assign/Remove and Your account on the Users and roles page. A Role assignments panel lists every fleet-wide grant (it needs
AccessReview:Readand writes the samerbac.assignments.listaudit row as the REST call). Administrators can Assign role and Remove a plain user grant; the server's refusals (last administrator, your own Administrator grant) are shown as they came, and the panel reloads after a server error because the change may have taken effect. The Users table gains an RBAC roles column and the Roles table an Assigned column, both derived from that panel and shown as "Not shown" unless the grant table was read in full. A Your account panel offers Change password (local accounts) and Sign out everywhere. -
Audit and reviews page, audit log (
/access/audit). The audit trail is now readable from the dashboard under Access: exact Principal and Action filters, a last-hour / 24-hour / 7-day window, 50-row pages with a stated 10,000-row paging limit, and a row detail with the event id and full detail text. It shows the same fields asGET /api/v1/auditand nothing more (no source address, no principal role), and an unreadable audit store is shown as a red box, never an empty log. The page also offers a user-initiated authentication sample (each draw is recorded asaudit.auth_sample.exported) and, on request, the audit retention period the store actually enforces ("Never" when nothing is cleaned up). The audit-log manual now describes the trail as access-controlled and retention-managed rather than tamper-evident, because no hash chain or signature backs the log. -
RBAC enforcement, your elevation and an own-permission check on the Users and roles page. An Enforce roles switch shows the server's setting to every signed-in user and lets an administrator turn it on or off with a confirmation and an MFA step-up; the switch is always redrawn from the server, shows its refusals with their remediation, and reads Unconfirmed when the setting cannot be confirmed. Your elevation raises or ends your own temporary admin session with a required reason, and Check your own permission asks whether your roles grant an area and operation.
-
Access reviews on the Audit and reviews page. The review campaign list, each campaign's frozen grants with a summary card, and the current grant population (on request, with a CSV download) now sit on
/access/audit;/access/audit?campaign=<id>opens a campaign directly. Reviewers withAccessReview:Attestcan open a review, attest or flag a grant and close a campaign from the page, through the existing/api/v1/access-reviewsroutes. Opening a review, closing a campaign and flagging a grant each ask for confirmation in a dialog; Attest records the decision as soon as it is clicked, and a decided grant offers no further controls on the page. Flag records evidence only and never revokes. Undecided grants are listed first, an unreadable store is shown as such, and viewing writes the same audit rows as the REST reads (access_review.list,access_review.get, andaccess_review.exportedwithformat=view). Settings keeps its Access Reviews panel until the follow-up that retires it. -
Roles and role permissions on the Users and roles page. A Roles panel lists the role catalogue and View permissions shows what one role allows, grouped by area. Both use the same permission (
UserManagement:Read) and the same checked read as the REST role endpoints, so an unreadable role store is a red message rather than an empty list, and an unknown role is told apart from a role with no permissions. Unlike the Users panels, a refused Roles load is recorded in the audit log as a permission-required denial. -
Audit read parity across REST v1, MCP and the legacy route.
GET /api/v1/auditnow acceptstarget_type,target_id,since,untilandoffset, returns each row'sid, and reports the realstartandpage_sizeinpagination. The MCPquery_audit_logtool gainstarget_idandoffset.target_idrequirestarget_type; malformed or out-of-range values are rejected with an error rather than ignored. Rows with equal timestamps are now ordered byid, so the order within one timestamp is fixed (the log is live, so a walk by offset can still repeat or skip a row when rows arrive or retention deletes them). -
DEX Apps list: per application and per version stability.
GET /api/v1/dex/appsand the MCP toollist_dex_appsnow carry, for each application and for up to 20 of its versions, crashes, hangs, devices affected,distinct_devices_online, the share of the Windows devices online now that are affected (95 percent interval), their events per 1,000 online devices (exact interval) and the transparent stability score with its decomposition, plus the population and its basis,apps_total(every application with a Windows crash or hang in the window) andapps_truncated(the list keeps the 100 applications affecting the most devices). No device floor applies, so figures are published at any device count. The share and rate count only affected devices that are online now (the same set the population counts); the crash, hang and device counts stay window-wide, and the score is withheld asoffline_affectedwhile any affected device is offline (the common case for multi-device applications on a fleet with laptops). A failed read now answers503(MCP: internal error with a retry hint) instead of an empty list, and so does a read over the four-at-once budget. The five pre-existing keys and the list membership are now Windows-scoped (the old read counted every platform): an application with only Linux or macOS signals is no longer listed and mixed applications show Windows counts; the Apps tab, the Overview top apps andGET /api/v1/dex/appstill count every platform until the tab is rewired. Additive otherwise. -
Fleet roster and per-policy compliance counts computed in SQL.
OfflineEndpointStore::fleet_state_countsreturns the roster total, heartbeat-stale count and a per-platform split (Windows, Linux, macOS, Other) from oneCOUNT(*) ... FILTER ... GROUP BY lower(os)statement, so no row crosses the wire and the 100,000-row roster cap no longer understates a large fleet; a failed read is a typed error, never a zero.PolicyStore::compliance_by_policy_checked(exposed asComplianceApi::compliance_by_policy) returns every policy's compliance counts from one cachedGROUP BY policy_id, statusread in place of one query per policy, andAgentRegistry::known_agent_countcounts the online set without building the id list.fleet_state_countsalso returns an exact SQLlivepresence count that stays correct past the 100,000-row live-id cap, whereknown_agent_countreports unavailable. The failure counteryuzu_server_agent_presence_store_failed_totalgains anop="fleet_state_counts"value. Both fleet-wide reads are unconfined by design: callers must apply the fleet-read gate and use the existing roster path for scope-confined operators. No route, MCP tool or migration changes. -
pkg_inventorygains a read-onlystore_packagesaction and real Linux legs.managerson Linux now reports which of dpkg, apt, rpm, dnf, pacman, apk, snap and flatpak are present (apt source-file, snap-file and flatpak app and runtime counts as facts; versions are not read);store_packageslists installed snaps (name, revision, version and type from the mounted revision's snap.yaml, active state) and flatpak apps and runtimes (id, arch, branch, active commit) from the system installation. Both legs are bounded, no-follow filesystem reads with no subprocess; an unreadable or incomplete listing is a constrained status with a token, never an empty result, while a snap whose snap.yaml cannot be read is reported with version-and kindunknown, and a flatpak whose active link cannot be read with stateunknown, rather than failing the action. The daily installed-software inventory collects the active rows as ecosystemssnapandflatpak(flatpak version is empty: a branch is a ref, not a version), and a constrainedmanagersanswer that still names a present manager no longer skips the whole cycle. macOS reportsstore_packagesunsupported by design (installed_appslists applications); Windows is planned. Also carries #4881's descriptor and wording corrections (unsupported legs declared rung 0, the Linux roster owner named asinstalled_apps.list_inventory). -
Settings gains an Integrations page.
/settings/integrationshosts the MCP and Gateway
panels (same fragment URLs and ids as on/settings, where both also stay). The MCP panel is one
form with a Save and a confirmation that names the state being saved; refused requests show the
server's message instead of failing silently. The page is admin-only and additive, and adds no
REST route or MCP tool. The Gateway panel's "Active Sessions" row is now "Agents via gateway". -
Integrations page: webhooks, offload targets and directory sync (System pages). Three new panels on Settings > Integrations, backed by six admin-only read fragments that also require the matching JSON route's securable (Infrastructure Read, Directory Read). Webhooks and offload targets list by destination host with the full URL on demand, show the newest delivery, open a delivery history, and can be created or deleted; creating one asks for a confirmation that names the destination host and the data the chosen events carry, and says it is not an approval. Directory sync shows Entra ID status from checked reads (a failed read, a never-synced directory and a failed run are separate states), a sync form whose tenant, client and secret are used for that request only, a group-to-role mapping editor that flags a mapping the server did not store, and a lazy, audited user list that is withheld if its audit row cannot be written. Every write calls the existing JSON route; there is no new write route and no new MCP tool.
-
System > Certificates and secrets (
/settings/pki). A new admin-only page under the Settings sub-navigation, built over existing data and permissions, with no new write route. It shows the certificate authority (trust mode, algorithm, validity, fingerprint, CA and CSR downloads, enterprise chain import), the revocation list (published time, next update, revoked count, and whether a revocation is missing from the published list), the issued-certificate inventory (the newest 500 certificates; older ones are found withGET /api/v1/ca/issued?offset=and revoked withPOST /api/v1/ca/revoke) with Expired and Revoked states and a purpose-aware revoke confirmation, and a read-only key-encryption-key status panel (GET /fragments/settings/kek, sameSecurity:Readgate asGET /api/v1/secrets/kek/status). The page shows an inline note, rather than an empty list, when a panel is denied or cannot be read; a revocation list past its next-update time shows as expired rather than current. The Settings page and its Internal CA panel are unchanged; the revoke and import-chain buttons on both pages now show a toast when the server refuses them after the same-site check (a cross-origin refusal still shows nothing). -
CRL figures on the certificate inventory.
GET /api/v1/ca/issued(meta) and the MCPlist_issued_certstool now carrycrl_version,crl_published_at,crl_next_update,crl_revoked_countandcrl_stale. Each isnullwhen it could not be determined (a CRL published before counts were recorded has anullcount, never0), so a client can judge revocation freshness without downloading the DER list. -
System > Plugins and updates (
/settings/plugins). A new admin-only page under the Settings sub-navigation, built over routes that already exist. It shows stored plugin configuration (read-only, with the note that no agent reads these values yet), a four-layer kill-switch lookup (all operating systems, Windows, Linux, macOS) that names the row that decided each layer, product packs, and hosts the existing plugin-signing and agent-update panels. Every new fragment applies the page's admin gate and the REST twin's own permission, and answers a denied or degraded read with an inline note rather than an empty list. -
System pages: Load generator start and stop. On Settings > Developer and API a Load generator panel starts and stops the generator on every connected device the caller can see. Start asks for confirmation. Every control calls a route that already existed and keeps that route's permission, audit and bounds; a refusal, such as a missing permission or the approval requirement, is shown as the server worded it. The devices-online figure is informational and does not enable or disable either button.
-
Tokens and principals page (
/access/tokens): create, rotate, confirm and revoke API tokens; create, mint, rotate, confirm, transfer, revoke and assign roles to engine principals. Changes call the existing REST routes, so step-up, validation and audit are unchanged; controls are drawn only for callers who can use them, and the checks are quiet for a browser session. A new secret is shown once in a strip that stays until "I have stored this" is pressed (no timer), never in a URL, history or storage, and the page refuses to mint a second secret over an unstored one.POST /api/v1/tokensandPOST /api/v1/upload-grantsnow sendCache-Control: no-storeon the response that carries the raw secret. -
Tokens and principals page (
/access/tokens), read side (the create, rotate and revoke actions are in the other/access/tokensentry in this release): list of API tokens (Active / All, rotation and expiry state, engine credentials marked) and of engine principals with their active credentials and roles, plus the no-administrator role check. The principal panels needEnginePrincipal:Read(the same permission asGET /api/v1/engine-principals; administrators only while RBAC is off), the revoked principal's free-text detail also needsAuditLog:Read, and the check runs only when the caller holdsAuditLog:Read. Administrators on a cookie session see every owner's tokens; every other caller, including an administrator's API or MCP token, sees their own tokens only. Refused browser sessions write no audit row. A read that does not answer is stated as such, never rendered as empty. New dashboard fragments/fragments/access/tokens/list,/fragments/access/principals/listand/fragments/access/principals/detail. -
Groups and enrolment page (
/groups, Estate group). One page for the agents waiting for approval, the auto-approve rules and the enrolment tokens. It shows the same panels as the Settings page, from the same routes and with the same Administrator gate; Settings keeps all three. Rules read as what they match, a rule that matches every agent is flagged, and Add rule asks for confirmation. A refused or failed action shows an error message instead of nothing. Management groups stay in Settings. -
Header notifications bell. Every shell page now carries a bell in the header that shows the unread notification count and a pop-over with the newest 10 notifications. Mark read and Dismiss each take a two-click inline confirm, because both remove the item for every operator and neither can be undone from the console. The bell uses the three existing
/api/notificationsroutes and adds no route or permission: it appears only for callers the server already admits (with RBAC on, global Administrator and ITServiceOwner; with RBAC off, every signed-in user, read-only for non-admins), polls every 60 seconds while the tab is visible and the user has been active in the last five minutes, and stops for the page after a401or403. Notification text is placed withtextContentonly. Read and dismissed state is shared by all operators, and there is no retention, so a large fleet can show "99+". -
The DEX score policy has a stored home and is named on every Apps payload.
GET /api/v1/dex/appsandlist_dex_appsnow carry apolicyblock (sourcedefault|custom, revision, the four tier edges and names, and afaultreason when a stored document is invalid and the shipped defaults are in force) and a per-applicationprofilemarker; the policy lives under the runtime-config keydex_score_policy, and each server instance refreshes it at most every 5 seconds while it serves requests, so a change typically reaches every instance within 5 seconds. When the store cannot be read, the last good policy is used for up to 60 seconds and after that the Apps read answers a503ending(score_policy_unavailable)instead of scoring under a guess.GET /api/configlistsdex_score_policyunder a newread_only_keysfield, and the genericPUT /api/config/dex_score_policyis refused with a400naming the policy route; the key is written throughPUTandDELETE /api/v1/dex/score-policy, and until a policy is written every response reportssource: "default". -
msi_packagesgains a read-onlylist_inventoryaction for the daily installed-software inventory (Windows). It lists machine-context Windows Installer products only — product code, name, version and publisher through MsiEnumProductsExW/MsiGetProductInfoExW — so a product installed for one user is never reported by this action. The daily sync collects the rows as a new ecosystem value,msi(kindpkg,package_idset to the product code), which now appears in the Software page, REST, MCP and CSV ecosystem values and filters; the product code is stored but not yet returned by the REST and MCP software routes. A Windows host normally lists each MSI product twice, once from this action and once from the registry walk, with no de-duplication. If the enumeration does not complete, or a product property cannot be read for any reason other than the product not having it, the action reports a constrained status and the daily installed-software report is skipped for that cycle (nothing is deleted and the last good state is kept). A product with no name, for example one that is advertised but not installed, is not collected. Install location and uninstall string are not collected (#5186). The existinglistandproduct_codesactions are unchanged apart from an exception guard; macOS and Linux report the action unsupported. -
New read-only
remote_access_auditagent plugin — who can log in to a host over SSH, on Linux and macOS. The singlelistaction reports thesshd_configvalues that govern inbound access as written (not sshd's effective policy), a SHA-256 fingerprint and the option names for every public key it can decode completely in theauthorized_keysof the homes it walks (/homeand/rooton Linux;/Usersowned by uid 500 or above and/var/rooton macOS), and whether thesshdservice is enabled. It never emits a key comment, an option value or a key-file path. A home or file the agent cannot read is reported as refused with a reason token, never as an empty result. Wheresshd_configsets a login source the plugin does not read (a certificate authority, a key command, a principals command or file, host-based authentication orAuthenticationMethods) to a value other than its inert one, the listing is constrained; the Linux leg, which runs unprivileged, is declared constrained. Stored results are readable byResponse:Readholders, as for every plugin. The plugin is Forensics-class: it ships off behind the plugin-config kill switch and is gated atForensics:Readwith approval, one device at a time. The Windows leg is a planned placeholder that reports oneunsupportedstatus row (windows:planned). -
Plugin enablement store (ADR-3006). The plugin-config store gains durable enablement rules, per-endpoint desired and reported state and a continuity anchor file
enablement-anchorbeside the server data. Keep that file with your backups: it is not in thepg_dump,scripts/yuzu-backup.shandscripts/yuzu-restore.shnow copy it, and a restore without it is reported as unwitnessed./readyzcarries anenablementobject and/healthanenablement_regressedflag; neither gates readiness. Newyuzu_server_enablement_*metrics and ayuzu-enablementalert group ship with it; until the later change that issues desired state, onlyYuzuEnablementRegressedandYuzuEnablementAnchorWriteFailingcan fire. No operator surface yet; rules are authored by a later release. -
Plugin output can be audited per action. The agent stamps
CommandResponse.action, and the server mintsregistry.writeaudit rows from aregistryplugin'saudit|lines. The agent stamps the dispatched action (the existing proto field 8, about 10 bytes per frame) on every plugin-output frame and on the terminal frame of every executed plugin command. The server turns theaudit|lines aregistrycommand streams from its mutating actions (set_value,delete_value,delete_key,set_user_value,delete_user_value,apply) intoregistry.writeaudit rows carrying the old and new values,principal=agent:<agent_id>and thecommand_id, which links each row to the dispatching operator's audit row where one exists. A row is minted only when the stamped action is one of those six, the server takes the plugin from its own dispatch record, which must beregistry(matched ASCII-case-insensitively), and thecommand_idis exactly what the server mints (registry-plus 16 or 32 lowercase hex digits); a command with no dispatch record mints no rows, and the content of eachaudit|line remains the agent's own report; a read action never mints one, and an agent that predates the stamp mints none, so during a mixed-fleet rollout only upgraded agents are audited. At most 300 lines of 4,096 bytes per frame are recorded and a frame past that is logged once, each agent id is budgeted to 600 rows per 60-second window on each server process, and what the server sees go unrecorded is counted byyuzu_server_registry_audit_dropped_total{reason}. The rows are written before the execution advances. A failed store write ends that frame's rows, is logged and movesyuzu_server_audit_emit_failed_total; the direct session also gets the gRPC audit-failed trailer once per stream (the gateway path logs it). At shutdown the server waits up to 15 seconds for a registry audit write that is still in flight before it releases the audit store, so the stacked shutdown bound indocs/user-manual/upgrading.mdis about 130 seconds, up from about 115.registry2.0.0 and later (this release ships 2.1.0) emit the lines; the server side is live on deploy and records the lines of any enrolled agent, up to the budget. The content of eachaudit|line is the agent's own report; the server checks the shape of thecommand_idand the stamped action and takes the plugin from its dispatch record. Seedocs/user-manual/audit-log.md(registry.write). -
Echo timeline renderer (internal). The server gains the Echo timeline view model (
echo_timeline_view.hpp: typed per-lane regions exact / rolled-up / pruned / paused / not readable / capped, lanes, moment and selected models, measured retention coverage kept apart from the agent's retention policy, the live process capture method) and pure renderers (echo_timeline_render.{hpp,cpp}) for the device bar, the what-is-kept chart, the lanes, the moment and selected panels and the "What this record cannot tell you" list, pinned by golden files and escaping tests, plus the timeline's.tl-CSS vocabulary inyuzu.css. The honesty copy is conditional on the read (polling interval, the process capture method reported by the agent, the Windows and macOS command-line note following the capture method (names only for the event streams, the executable path for polling), DNS without a process, offline reads as not readable, a read cut off at a row limit says so, no expiry date is derived from observed history). No page, route or API change yet; nothing is operator-visible. -
registry2.0.0: typed, verified writes on a link-checked handle.set_valuenow writes all six value types (REG_SZ,REG_EXPAND_SZ,REG_BINARY,REG_DWORD,REG_MULTI_SZ,REG_QWORD) from text or exactdata_hexbytes, captures the previous value, reads the new one back on the same handle and fails on any difference (verify_mismatch). Every mutation runs on a checked handle: onHKLMandHKUone open whose canonical kernel name is compared with the requested path (a registry symbolic link anywhere in the path changes that name and sends the call to the reference component walk), onHKCUandHKCR, for a missing key and whenever the open cannot decide, the walk itself, which refuses a link (key_is_link), creates missing components, refusesconcurrent_changeand hands over a handle only when its canonical kernel name is the requested path, so a link completed after its probe cannot redirect it (a creating write whose walk does not hand its leaf over, or that raises an exception before its verified write ends, tries to delete every key it made, by handle, leaf first, as a best-effort rollback: it keeps any key that holds a value or key when it checks, and the audit line'screated=reports every key left on a returning exit). Keys are deleted by handle withNtDeleteKey, so a swapped path cannot redirect a write or a delete;delete_keystill removes only a key with no subkeys and now reportscounted_keys,counted_values,dry_runandcapped(the count stops at 10,000 nodes: the key plus up to 9,999 values). Every mutating action emits oneaudit|line with the old and new values (hex of the raw bytes, capped at 256 bytes; the product owner accepted this risk on 2026-10-20 and masking is a planned later change, so the rows are readable byAuditLog:ReadandResponse:Readholders (Viewer, Operator, PlatformEngineer, ITServiceOwner and Administrator), the legacy/eventsstream (any authenticated non-service-scoped session) and, with the write parametersvalueanddata_hex,executions.parameter_values, which no server retention pass prunes; audit rows and responses are kept 365 and 90 days by default), which the server records as aregistry.writerow when the agent stamps the action; an action that ends in an exception leaves a fixed failed audit line, so that row exists too; the line grammar and the producer contract are published in the plugin README. The macOS and Linux legs are declared as planned stubs. -
/swaggerAPI reference page in the dashboard. It renders the OpenAPI document served at/api/v1/openapi.jsonas a searchable reference grouped by tag, with method, group and typed-response filters, an expandable view of each operation's parameters, request body and responses, and deep links such as/swagger#GET%20%2Fhardware. Each of the 47 groups opens with a guide (what it is for, how its data gets there, what it holds, permissions and audit records, caveats, and its data model), carried in the document as the top-leveltagswith anx-yuzu-guideextension. The page takes the sameInfrastructure:Readgate as the spec route, has no sidebar entry, and marks operations whose 200 response has no typed schema as untyped and operations the running server does not serve (the License Management, Device Tokens and Software Deployment groups) as unavailable, using anx-yuzu-unavailableextension on the operation.GET /api/v1/openapi.jsonleaves the guides out by default and includes them with?guides=true, so the MCP resourceyuzu://openapiand the discover tools, which serve the default form, do not carry them. -
Echo timeline page shell (internal). The server gains the hidden timeline page (the route is registered only when
kEchoTimelineUiis on; it is off), its interaction script (drag, click and keyboard scrubbing of the moment line, Play/Pause with 1x, 10x and 60x speed through the record already loaded, Previous/Next event, Copy link to this moment, moment and selected panels requested from the timeline fragments) and the timeline markup attributes the script reads. No route is reachable and nothing is linked from Echo; nothing is operator-visible. -
New MCP tool
get_me, the twin ofGET /api/v1/me. An agentic worker can read its own identity, how it authenticated and its directly assigned RBAC roles without a permission probe.GET /api/v1/meandGET /api/megainprincipal_kind(humanorengine),auth_source(always present),mcp_tier,scope_service,rbac_rolesandrbac_roles_resolved.get_merefuses service-scoped tokens; the REST route still lets them describe themselves. Refs #2146. -
Echo
registrysource ($Registry_Live, Windows, on by default). Echo now records changes to a curated set of registry keys (Run and RunOnce, Services, Image File Execution Options, AppInit_DLLs, Winlogon, LSA, Policies and Defender under HKLM, and the Run, RunOnce, Policies and Winlogon keys of every loaded user hive): each added, changed or removed value with its old and new data, and each removed key. A key watch finds what changed and a baseline diff gives the old and new data; the process that made a change is not recorded by this entry (actor_pidis 0); the "Echoregistrysource names the process that wrote a change" entry in this release adds it. Values are stored unmasked: every value under a watched key, changed or not, is readable throughecho.sql. Retention is 500 000 rows; opt out withecho.configure registry_enabled=false. Opting out, and rolling back to an older agent, both leave the existing rows and the unmasked per-key baselines inecho.dbuntilecho.purge_sourceis run on the paused source; run it before a rollback, since an older agent cannot. Device pages show the new source under aSystemcategory. -
Echo retention tiers on the data-retention API.
GET /api/v1/settings/data-retentionnow carriesecho.tiers, the per-source retention tier defaults an agent of this server's release applies (row limit or seconds, roll-up bucket, guaranteed depth: null for row-count tiers), plus the 900 s prune tick. The table is a checked-in mirror of the agent's schema registry with a drift test; it is published as defaults, not per-device readings. -
Echo
registrysource names the process that wrote a change.$Registry_Liverows now carryactor_pidandactor_image: a write-only Kernel-Registry ETW session (value writes, value deletions and key deletions only) reports which process wrote a value, and each change the key watch finds is matched to the writes of the same value name, type and size inside the window in which it must have happened.detailsays how the actor was reached:attr:corrwhen exactly one process wrote a matching value, and, with no process named,attr:ambiguous:<n>,attr:none,attr:degraded(write events were lost, with agap:loss:<n>row),attr:offorattr:lost-restart. The actor is a lead to verify against$Process_Live, not proof. The ETW session needs an elevated agent; without one the key watch runs alone and rows sayattr:off. A row now lands in the tick that sees the change, or the next one when the write was within a few seconds of the tick or was found by the hourly re-read, once its writer is settled, and a change that has to wait does so in the source's cursor row (old and new value readable throughecho.sql).echo.statusgainsregistry_etw_events,registry_etw_dropped,registry_etw_lost,registry_attr_attributed,registry_attr_ambiguousandregistry_attr_none, andregistry_capture_methodreadsnotify+etwwhile the session runs. -
Echo retention tiers on Settings > Data and retention. The data-retention panel now shows a read-only table of the Echo capture sources' retention tiers (exact, hourly, daily, monthly, boot) and a "Look again after" floor per source, read from the same model as
GET /api/v1/settings/data-retention; agent defaults for this release, not per-device readings. -
registry2.1.0: per-user hive writes and a declarativeapplywith a returned undo manifest.set_user_valueanddelete_user_valuewrite or delete one value in a user's hive, resolving the profile asget_user_valuedoes, except that a write never resolves a<SID>.bakleftover: through the liveHKEY_USERS\<SID>hive when the user is logged in, otherwise through the offlineNTUSER.DATbehind the shared hive-file guard, under the offline-hive lock, with the same typed, read-back-verified write asset_valueand aRegFlushKeybefore the unload (a flush that fails fails the write). A hive loaded after an offline write returned is reported as a warning; the audit row nameshive=HKUand the profile'ssid.applyruns a plan of up to 256HKLMset_value/delete_value/create_key/delete_keyitems: a dry run by default, one checked handle per distinct key, one verdict function for the dry run and the real run, refusal before any mutation of an item whose previous value this plugin could not write back or whose key path or value name a row could not spell exactly (unsupported_old_type,unrepresentable_name), and an undo manifest in the reply (the previous value of each item, the values of each deleted key) bounded by one 512 KiB budget, withundo_completein the summary row; nothing is persisted on the endpoint. A plan is refused whole when two items depend on each other (dependent_items). The plan is not transactional: it stops at the first refused or failed item unless told to continue, and the manifest restores values only (not a deleted key's security, class or volatility). Each plan item attempted is audited as its ownregistry.writerow; items skipped after a stop have none.applyanddelete_user_valueare Destructive, so they need explicitagent_idsand refuse scope and broadcast targeting. Three new definitions:windows.registry.set_user_value,windows.registry.delete_user_valueandwindows.registry.apply. -
Shared SMBIOS walker in agents/shared (smbios_parsers.hpp): one bounds-checked, never-throwing decoder for structure types 0 (BIOS), 1 (system), 2 (baseboard), 3 (chassis), 4 (processor) and 17 (memory device), accepting both the 8-byte RSMB framing and the headerless Linux DMI table, with a placeholder-string filter; the firmware_posture type-0 decode now runs on it with byte-identical output.
-
Hardware-estate scaffold (agent).
hardwaredeclareschassis,baseboard,swapandmachine_id;disk_actionsdeclarespartitionsandtopology. Every new action answers one typedstatus|<action>|unsupported|<os>:plannedrow on every OS until its leg lands. -
The shares plugin now reads what a Windows host serves over SMB. list on Windows enumerates the live shares through NetShareEnum (level 2; level 1 names without paths when level 2 is refused): name, path, kind (disk, print, device, ipc, other), with temporary shares flagged temporary, the administrative ADMIN$, C$ and IPC$ shares flagged special rather than hidden, and any other share whose name ends in
$flagged hidden; a stopped Server service is reported, per the API contract, as unavailable with a named token, never as an empty list; the enumeration is not a snapshot: a share name returned twice is kept once, and a multi-page enumeration whose final count differs from the first page's total is reported as constrained with a named token; ACLs are never read. The macOS leg is still planned. -
Groups and enrolment page shows management groups. The management-groups panel on
/groupsnow lists the group tree with each group's type and member count, and selecting a group shows its details (parent, description, creator, dates and the stored rule of a rule-based group) and its members. The panels are read-only; groups are still created and deleted in Settings. A member list shows only the members within the viewer's scope and states that without revealing how many were left out. A group read that fails shows a "could not be read" box with Retry instead of an empty tree or "0 members"; a failed group-list read now counts onyuzu_server_mgmt_group_read_degrade_totalwherever it is made. The tree draws at most 500 groups and a member list at most 200 rows, with a note when more exist. -
macOS Endpoint Security development bundle. Adds an opt-in signed app-package lane with strict profile preflight, immutable-runtime updater safeguards, external plugins, and recoverable installer transitions.
-
Added a macOS development foundation runbook, private-evidence build-context exclusions, Provisioning UDID helper, and opt-in single-target installed-agent ES smoke check for subsequent Spark, Guardian, DEX and Reflex development; no consumer cutover or production-readiness claim.
-
Software inventory store accepts the extended software row.
installed_softwaregainspackage_idandsource(the plugin action that produced the row) plus a row id for keyset paging (migration v7, additive). The ingest seam parses 16-field blob records while 4- and 12-field records from older agents still parse; the 13th and 14th slots (install_location,uninstall_string) are reserved and are neither stored nor hashed until ADR-0016 §8 is re-opened for them. The server-side canonical hash covers the new fields only when present, so existing agents keep hash-skipping. The store query gains a case-insensitiveqfilter over name, publisher, ecosystem and source, exactkind/ecosystem/sourcefilters and a keyset cursor (store-level only; no REST/UI change in this release). Deploy the server before any agent that emits the new fields (package_idandsource; an agent that fills the reserved slots 13-14 is asked for a full resend every cycle until the server accepts them). On first start after upgrade the migration rewritesinstalled_softwareunder an exclusive lock, in time that grows with its row count (about 10-20 s at 4M rows); start one server replica first and let it finish before starting the rest, and above about 2 million rows stop every replica before starting that one (see the Upgrading note in the inventory manual). A server whose database is missing a column the software store queries now refuses to start with a clear log line instead of failing on the first request. -
The daily installed-software inventory now collects from every inventory action on the host. Besides
installed_apps(list_inventory), the agent reports Homebrew formulae and casks plus one Homebrew presence row frompkg_inventory(packages/managers, ecosystembrew) and every Windows optional feature, enabled or disabled, fromwindows_optional_features(list, ecosystemoptional_feature, the DISM state in theversioncolumn). Every row now carriessource, the producing plugin action;package_idstays empty for these producers and the reservedinstall_location/uninstall_stringslots are still sent empty. An action that is not loaded or not supported on the OS is skipped (exceptinstalled_apps: without it the source stays idle, because it anchors the report); one that fails skips that day's report and keeps the last good inventory. Each agent sends one full report after upgrade (its content hash changes), at its existing phase-spread slot (about 24 h), then hash-skips as before; deploy the server (the extended-row store) first. The server has no admission control for inventory resends yet (planned, not built), so the lockstep cases are agents with no persisted sync state, agents already overdue at reconnect, and a fleet-wide "Sync now"; staging by management group is advisable for very large fleets, not mandatory. Disabled optional features are listed too, so the/softwarecatalogue count (grouped by name) counts a host with a disabled feature as having it (see the inventory manual). -
Gateway: a slow server is now defined, exported as metrics and alerted on. The gateway measures the share of time its path to the server is backed up (the upstream process inside an RPC with work waiting, or all stream-status notification or Guardian forward slots held) and exports it as
yuzu_gw_upstream_backpressure_seconds_total(whoserate()is the duty cycle),yuzu_gw_upstream_lane_saturated_seconds_total{lane},yuzu_gw_upstream_backpressure_duty_cycle(last 60 s) andyuzu_gw_upstream_slow(1 at a duty cycle of 0.5 or more). The new warning alertYuzuGatewayUpstreamSlowfires when the flag has been 1 for 5 minutes. The breaker still counts only failed calls (#5681)./readyzno longer waits for the upstream process: itscircuit_breakercheck reads the breaker state that process publishes, so a slow server no longer turns/readyz503 with the breaker closed (#5680). See "Slow server" in the gateway manual. -
The Executions and Approvals pages can now act, not just list. On
/executions, the detail pane shows per-device results, a running execution updates live over SSE (with a stream-state chip), and Cancel execution opens a confirmation dialog. Unconfined operators also see fleet-wide figures (today, all time, device success rate, average duration), hidden when the store cannot be read. On/approvals, Approve and Reject open a dialog with an optional reviewer comment, and the sidebar pending-approvals badge refreshes after each decision. The shared dialog module (/static/yuzu-dialog.js) is added for later pages to reuse. -
Executions and Approvals pages. The sidebar now lists Executions and Approvals under Instructions as list pages (
/executions,/approvals; the "The Executions and Approvals pages can now act, not just list" entry in this release adds the actions) with filters, a detail panel for one execution, and a pending-count badge on the Approvals entry. The Executions and Approvals fragments answer refusals, store failures and unknown filters as200notes (data-denied,data-degraded,data-invalid); an HTTP-status monitor of those fragments no longer sees them as errors. -
Device containment on the Hardware page. A Quarantined devices panel, a Quarantined pill on
list rows, a record-header banner and a Containment tab show which devices have an active
quarantine record and how far containment has been shown to reach them (recorded, applied, or
confirmed), and let an operator withSecurity:Executequarantine or release a device from a
dialog. Release ends the server record only and says so. The panel and list pills make oneSecurity:Readlist-gate decision per view and apply its
scope in the store query, the same rule asGET /api/v1/quarantine, so a confined account sees
only its own records and an out-of-scope record costs no per-record permission check or audit
row. The page route now forwards a validatedlensso a link can open a specific tab. -
Dashboard and Result sets extras. The Dashboard gains a scope expression helper (Validate, Preview limited to 25 ids, Scope kinds), a Run on Echo history form that shows the SQL before it runs, a confirm before any send that reaches more than one agent, and masking of
grant_secretandgrant_idin the History list.POST /api/dashboard/executeaccepts an optionalscope_expressionfield to target a send at an expression; an attribute the resolver does not know is refused withscope_unknown_attribute, because!=andNOTwould otherwise match every device. Result sets shows Source, Devices, Status, Captured and Expires, a failed or degraded list as such, the member ids of a set, and creates sets from an instruction, an inventory query or an Echo query, or re-evaluates one into a new set. Two new read-only fragments,GET /fragments/result-sets/{id}/membersandGET /fragments/result-sets/producers, answer refusals and store failures as200notes (data-denied,data-degraded). -
DEX application performance view shows crashes and hangs per version. Under the per-version CPU and memory table, an application opened on the whole-fleet scope now carries a table of crashes, hangs, affected devices, exposed devices, the failure rate with its 95 percent interval and a reliable marker, and the rate ratio against the previous version. It is the same read as
GET /api/v1/dex/perf/appand theget_dex_app_perfMCP tool, so the page and the API cannot disagree. Every rate is printed beside the exposed-device count it was computed from; exposed devices are sampled devices, not a census; an unknown exposure reads "unknown", never 0; a ratio with no upper bound says so; the window ("last N days"), the 20-version cap and a non-zerostat_failuresfault are all stated. No figure is labelled good or bad. A degraded crash-and-hang read keeps the performance table and shows a failed-read note with a retry link instead of a table of zeros. A management-group or device-model scope does not show the table, because failures are recorded fleet-wide. -
Dashboard readout, Needs attention and Estate state.
/fragments/dashboard/summaryfills a
four-cell readout (devices online, policy checks passing, failed executions in the last 24 hours,
approvals waiting), a Needs attention list with links to the page that owns each item, and an
Estate state panel (seen in 30 days, online, offline, not seen for 2 days or more). Each figure
keeps its own permission, accounts limited to a group of devices see a "limited" note instead of
fleet totals, and a figure that cannot be read says so rather than showing 0. -
Dashboard: Compliance by policy, Recent events and Server health panels. Three dashboard
fragments (/fragments/dashboard/policies,/fragments/dashboard/activity,
/fragments/dashboard/health) fill the Dashboard's lower rows. Compliance by policy lists the five
enabled policies with the lowest pass rate and shows "No results yet" for a policy that has not
reported, never 0 percent. Recent events is a five-column table (time, who, action, target,
severity, the last being the audit row's own result) of the eight newest audit rows, shown to
administrators who holdAuditLog:Read;detail, source address, user agent and session id are
never shown. Server health reuses the/api/healthdocument, so the panel and the probe agree on
what "healthy" means. Every panel answers 200 with a distinct note for a refused, limited or
unreadable read, and a service scoped token is refused first. No new REST route or MCP tool. -
The Instructions library lists more than the first 100 definitions honestly, and instruction set create and delete are audited.
GET /fragments/instructionsacceptsname,plugin,typeandlimit(default 100, maximum 500) and shows "Showing the first N. Narrow the filter." when the list is cut; a store failure reads "This is not an empty library." instead of an empty table. When the caller holdsInstructionDefinition:Readthe table gains a "Needs approval" column, and when the caller holdsInstructionSet:Readit gains a read-only sets panel with member counts; otherwise the call is unchanged apart from the sets panel's denied note.POST /api/instruction-setsnow writesinstruction_set.createrows for success, store failure, conflict and rejected input, andDELETE /api/instruction-sets/{id}writes aninstruction_set.deletesuccess row (a failed audit write setsSec-Audit-Failed). -
The Instructions page gains sets, response templates, import/export and expression-targeted dispatch. The Definitions tab has a name/plugin/type filter, a New set and Delete set panel with the set assignment on each row, an Import definition dialog (the JSON export document; the unsigned and duplicate-id refusals are shown), per-row Export (JSON) and Templates.
GET /fragments/instructions/templates?id=lists a definition's response templates behindInstructionDefinition:Read(a denial is an HTTP 200 note) and the template create, edit and delete dialogs call the existing REST v1 routes. New, Edit, Delete and the set controls now follow the caller's permission, probed once per capability per render, instead of the administrator role test. The Execute form reads parameters fromGET /api/v1/instructions/{id}(so they render), showsgrant_secretandgrant_idas write-only password inputs, can target a scope expression with Validate and Preview, and confirms with the instruction, target and "will be submitted for approval, not run" when the definition needs approval. Denied permission probes are audited as before, so a page load by a caller lacking a capability writes one denial row per missing capability. -
Guard edit and delete on the Guardian Guard page. Users with
GuaranteedState:Write/:Deletecan edit a Guard's name, severity and enabled state (Mode is shown read-only; last write wins) and delete a Guard by typing its name, from the dashboard. Both need an MFA step-up, are refused for service-scoped API tokens and cross-origin requests, and use the same audit verbs as the REST and MCP routes (guaranteed_state.rule.update/.rule.delete). Deleting does not unarm agents that are already enforcing the Guard until the next push. -
The device record shows application usage and detected licences on demand, and the Hardware
list gains fleet readouts. The Installed software lens has a header ("N applications, last
synced ...") and two load-on-click panels, Application usage and Detected licences, that apply
the same scope gate and audit-before-read rule as their REST twins; a view whose audit record
cannot be written is withheld. The Hardware list shows median memory and a by-manufacturer count
over the devices you can see, and the Software page lists Inventory sources (collector, device
count, last collected) for callers with a fleet-wide Inventory read. -
The device record's Tags lens is now "Tags and properties" and lists custom properties and
property schemas. A new fragment,GET /fragments/hardware/ci/properties?id=, renders the
device'sprops.<key>values and the fleet-wide schemas read from the existing dashboard routes.
A denied read names the missing permission and a degraded store says so instead of showing an
empty list. Remove on a tag is now offered only to a caller holdingTag:Delete(previously any
Tag:Writeholder saw a control that could only be refused). -
Stored data removal on the Hardware record. The Overview tab of a device record has a
Stored data panel, shown on click, that erases the device's stored rows through the existing
auditedDELETE /api/v1/sle/agents/{agent_id}route after you type the hostname and confirm.
It says what is and is not removed, lists per-store outcomes when a removal is incomplete, and
needs theDecommission:Deletepermission for the device. -
Workflows page: run, create and delete workflows. On
/workflows, New workflow takes the workflow's YAML, Run now opens a device checklist (with a device-id fallback when you may not list devices) and Delete retires a workflow after a confirmation. Controls you may not use are shown disabled with the permission they need. If the server takes longer than 8 seconds to answer a run, the page says the run may keep running, disables that workflow's Run now and watches its newest run for up to 10 minutes. Step rows show an instruction's name only when you also holdInstructionDefinition:Read. -
Workflow runs on the Workflows page. Each workflow now shows its Last run and Result, a Latest run panel (status, duration, device count and one row per step) and a Recent runs list, behind the same fleet-read scope as
GET /api/v1/workflow-executions. Backed by the newGET /fragments/workflow-runsandGET /fragments/workflow-runs/{id}fragments. A run outside your device scope looks the same as a run that does not exist. -
Workflows page: manage schedules. Schedules on
/workflowscan be enabled (after a confirmation that it will run without anyone present), disabled, deleted and created. Controls are disabled on schedules you did not create. New schedule picks an enabled instruction, a cadence, targets (empty targets need an explicit All devices acknowledgement), approval and parameters, and ends with a summary that includes the instruction's approval mode. -
Workflows page. The sidebar now lists Workflows under Instructions: a page (
/workflows; this entry lists only, and the other Workflows page entries in this release add run, create, delete and schedule management) with the workflow definitions and their steps and the recurring schedules, including Count and Last run. Backed by the newGET /fragments/workflowsand aview=pagemode onGET /fragments/schedules; the default Schedules tab output is unchanged. A schedule created by someone else shows only All devices or Targeted. -
Deployments tab on
/auto. The Deployments page (/auto) gains a read-only Deployments tab listing your deployments with a drill into each one's device grid, a visible message whenSoftwareDeployment:Readis missing or the read fails, and a truthful note on what advances a running deployment. -
The Network quality page has a Discovered tab. Next to Quality, it lists hosts found by network discovery scans (address, name, MAC, subnet, found by, last seen) from the existing discovered-hosts fragment. A reader without
Infrastructure:Readsees a message naming the missing permission, and the tab is present even when no device reports network telemetry yet./network#discoveredopens it directly. -
GET /api/v1/config,PUT /api/v1/config/{param}and the MCP toolsget_runtime_configandset_runtime_config. The runtime-configuration API now has v1 REST and MCP twins of the legacyGET /api/configandPUT /api/config/{key}. All of them share one implementation, so the per-key caller rule from the administrator-only key fix, the value checks and theconfig.updateaudit rows (denied, orattemptedthensuccessorfailure) are the same on every surface. The v1 routes answer in the A4 envelope.PUT /api/v1/config/{param}needs a fresh MFA step-up proof for an MFA-enrolled local or OIDC cookie session, and, likeset_runtime_config, answers aneffectfield (immediate,deferredorstored_only) becauseapplied: trueonly means the value was stored: the retention windows,auto_approve_enabled,plugin_signing_requiredand theoidc_*keys other thanoidc_admin_groupandoidc_redirect_uri(which wait for a restart) arestored_only. An MCP-tier token can write only the five keys that are not administrator-only (heartbeat_timeout,response_retention_days,guardian_event_retention_days,auto_approve_enabled,log_level); the rest answer a permission error with adeniedaudit row (dex_score_policyanswers invalid params). -
Leader-election metrics and alerts (HA WS-11, #4014). Every server with a Postgres-backed leader
elector now exportsyuzu_server_leader_is_leader,yuzu_server_leader_epoch,
yuzu_server_leader_acquired_total,yuzu_server_leader_acquire_failures_total{cause},
yuzu_server_leader_lost_total{cause},yuzu_server_leader_last_ok_age_secondsand
yuzu_server_leader_loop_errors_totalon/metrics, published at scrape time so a stalled coordination
connection cannot stall a scrape.docs/prometheus/yuzu-alerts.ymlgains theyuzu-ha-leadergroup:
YuzuLeaderSplitBrain,YuzuLeaderNoLeaderandYuzuLeaderCoordinationStale(critical) and
YuzuLeaderFlapping(warning); reload your rule file, and scrape each replica individually. Leadership is
deliberately not part of/readyz, so these alerts are the only signal that the leader-only background
passes are paused; like every rule they need a successful scrape, so also alert onup == 0for the
server job (none ships). Replicas on different databases are not fenced against each other, which is the
caseYuzuLeaderSplitBraincan observe. Theleader_elector: acquired leadershiplog line now carries theholder_id. This does
not lift the second-replica safe-to-scale gate; remediation-claim counters (#6526), a CRL freshness gauge
(#6527) and a durable cluster-capability admission gate (#6528) are tracked separately. -
New built-in
ForensicsAnalystRBAC role: a read-only way to delegateForensics:Read(#4990). It holds exactlyForensics:Read,Response:Read,Execution:ReadandInventory:Read, with noExecuteand no write, so a holder can read the stored responses of Forensics-tier plugins but cannot dispatch anything; collecting forensic data stays approval-gated. Assign it fleet-wide with RBAC enforcement on, through the Users and roles page,POST /api/v1/rbac/roles/ForensicsAnalyst/assignmentsor the MCPassign_rbac_roletool; it is created at server start, assigned to nobody, and does nothing while RBAC enforcement is off. Management-group-scoped assignment and engine principals are not supported, and a hand-authored management-group scopedForensics:Readgrant reads nothing from the stored-response routes. If a role with that name that is not a built-in role already exists, it is kept as it is, the grants are not added to it and the server logs an error at start-up; remove it (a role with grants or holders cannot be renamed; create a replacement under another name first if its holders need one) and restart to adopt the built-in role. -
local_security_policy plugin: local password, lockout and audit policy posture (
password_policy,lockout_policy,audit_policy), read-only. Linux reads/etc/login.defs,/etc/security/pwquality.conf/faillock.conf,/etc/pam.d/*and/etc/audit/audit.rules; macOS runspwpolicy -getaccountpoliciesand reads/etc/security/audit_control. A refused read reportsPERMISSION_DENIED/CONSTRAINEDwith a named token, never absent. -
/test --instructionsrunner can drive Destructive and Forensics definitions at one chosen agent.scripts/test/instructions_runner.pygains--target-agent-id,--confirm-target(the agent's hostname, checked againstGET /api/agentsbefore anything is sent),--manifestand--allow-remote, and those classes now require the existing--params-overrideand--run-id. They are opt-in, run one at a time against exactly one agent on a loopback dashboard (unless--allow-remote), and only with an explicit parameter entry per definition. Every dispatch is written to a manifest (created exclusively, never reused) before it is sent. Only a 4xx, a 503 with a known pre-dispatch reason, a pending-approval 202 or a refused connection mean "not executed"; anything else ends the run asdispatch_unknown, including the generic 503 "no agents reached". Ctrl-C, Ctrl-Break (Windows) and SIGTERM stop dispatching and exit 130. A run without those classes (the/testdefault) dispatches as before, but the runner is stricter for every run: each request carries ayuzu-instructions-runner/<run-id>User-Agent, proxy environment variables are ignored, a redirect on the execute POST is not followed, a missing or malformed--params-override, an invalid--run-idor a--dashboardwith userinfo exits 2,--outputrefuses a symlink or a multi-link file, and SIGTERM stops the run like Ctrl-C. See/testSKILL.md, "--instructionsmode". -
Per-OS plugin kill-switch rows. The server-side plugin kill switch can now be narrowed per agent OS (
windows/linux/darwin): a per-OS OFF row withholds dispatch of that plugin/action to agents of that OS, including agents connected through another replica, while other OSes keep the plugin-level state; a per-OS row only ever narrows, and existing plugin/action rows are unchanged on upgrade. An agent whose OS is unknown or empty is never withheld, the OS is agent-reported and matched exactly, and the MCP dry run cannot see the per-OS layer. Rolling back, or running an older replica, drops per-OS enforcement and an older replica applies a per-OSPUTas the plugin-level row (see Upgrading). The kill-switch REST routes and MCP tools accept an optionalos(a present-but-blank?os=on REST answers400, and a non-stringoson MCP is rejected as invalid params). A dispatch withheld this way answers503withreason: kill_switched_os, is recorded in the audit log ascommand.dispatch_withheld, and is counted onyuzu_server_dispatch_target_rejected_total{reason="kill_switched_os"}. When agent presence cannot be read while a per-OS switch is set, the dispatch fails closed with a transient503reason: os_gate_unreadableandretry_after_ms: 5000, is recorded ascommand.dispatch_withheld, and on the scheduled path is counted onyuzu_server_command_outbox_deliver_retry_cause_total{cause="os_gate_unreadable"}. Store schemaplugin_config_storemigration v2. -
yuzu_fleet_inventory_sync_skipping{source}gauge. Agents currently reporting heartbeats whose daily-sync source skipped its most recent collection cycle(s), derived from the agent heartbeat tagyuzu.sync.<source>.skip_streak(todaysource="installed_software"). Read it alongsideyuzu_inventory_stale_agents{source}: a host counted here is online but its collector is skipping (the cause is in the agent's log, and in itsyuzu.sync.<source>.last_skipheartbeat tag, which no server page or API returns yet). The two gauges count different populations (live heartbeats vs stored-receipt age over 48 h), so correlate them; do not subtract one from the other. Published every fleet-health sweep, 0 included. Agents emit the tag from the installed-software skip-visibility change (#5327); agents without it (older agents, or during a rollout) contribute zero even while their collector is skipping, so a zero does not prove every collector is healthy. (#5332) -
Local password change and admin reset (#5342). A local account can now change its own password — Settings → User Management → Change password, or
POST /api/v1/users/me/passwordwith the current password — and an administrator can reset another local account's — Reset password, orPOST /api/v1/users/{name}/password. The reset requires a durable Administrator (plus MFA step-up when enrolled): with RBAC off, the caller's own local account must hold theadminrole; with RBAC on, the caller needs a user Administrator grant. A JIT elevation, an IdP-group admin role or a custom role holdingUserManagement:Writeis refused403(durable_admin_required). Both routes accept interactive dashboard sessions only (API, MCP, engine and service-scoped tokens get403), require a same-originOrigin/RefererandContent-Type: application/json, and enforce one password policy of 12–1024 bytes (UTF-8), now also applied to account creation;/loginanswers an over-max password as a wrong one without hashing it. A wrong current password counts toward account lockout, and a locked account gets the same403. Each change or reset is one database transaction: the new password, the sign-out of every session of the account on every server (your own included after a self change), the discarding of an unfinished MFA enrolment, the target's lockout (reset only;lockout_clearedistrueonly when there was one) and the audit row are saved together or not at all — a refused or failed request changes nothing, and a session proven with the old password can neither survive the change nor be minted while it commits. An enrolment started under the old password cannot be completed afterwards. After a self change you are signed out and sign in again with the new password. A reset leaves the target's API tokens and any enrolled second factor in place, reporting how many tokens remain (api_tokens_active, ornullwithapi_tokens_unknownwhen the token store cannot be read). The configured break-glass account cannot be reset (403break_glass_target); under--auth-mode=sso-onlyonly an armed break-glass account may change its own password. Audited asuser.password_change/user.password_resetinside that transaction; if the row cannot be written nothing changes and the request fails503(audit_unavailable,Sec-Audit-Failed). SSO and SCIM accounts are refused409. There is no MCP tool for either action (parity-ledger exception; a reset tool with a server-generated temporary password is #5357). The Windows server installer now refuses/ADMIN_PASSor/OPERATOR_PASSon an upgrade with exit code 11 instead of silently ignoring it (#5274), skips its account pages on an upgrade, and rejects a password over 1024 bytes. New metricyuzu_auth_password_changes_total{kind,result}. The Settings "Sign out everywhere" button's post-logout redirect, which the dashboard CSP had been silently blocking, now runs. -
Guardian (agent and server): Spark claim-lifecycle health telemetry on the heartbeat and as fleet gauges. Dormant until
prefer_sparkis enabled, which no released build does, with one exception:yuzu.guardian_ack_maint_exceptionshas a push-apply source that runs on every agent and ships only if that step throws. Agents gain 13 sparse count tags (a zero count ships no tag) and two age tags,yuzu.guardian_disarm_pending_age_secondsandyuzu.guardian_compensation_pending_age_seconds(absent while nothing is pending, never a fabricated0). The server exposes them as 13yuzu_fleet_guardian_*fleet sums and two_maxage gauges, and counts forged values inyuzu_fleet_guardian_health_tag_rejected. A Disarm pending past a fixed 30 s, and a compensating teardown outstanding past its deadline (5 s after it became owed in production), are each counted once per claim and logged. This is observation only: nothing is released or cancelled, and no alert rule ships. The agent's Guardian heartbeat block is also now split into independently contained groups (journal maintenance, legacy-sink kick, theyuzu.guardian_generationtag, the older Guardian tags, and the new claim-health tags last), so a throw such as an allocation failure is contained instead of escaping the heartbeat loop, and no longer drops later tags. In particular the generation tag the server uses to trigger its missed-push reconcile survives a fault in any other group. Each group logs its first failure and every 100th with a running count; no fault metric ships yet. Upgrade the server first so the gauges exist when the first upgraded agent reports (#5403, #5404, #4472). -
New critical alert
YuzuRbacBreakerOpen(#3526).docs/prometheus/yuzu-alerts.ymlnow ships a per-replica rule that fires whenyuzu_server_rbac_breaker_openhas stayed at1on a replica for 5 minutes, meaning that replica's authorization reads keep failing and its fail-fast breaker has not closed. It complementsYuzuRbacReadDegraded, which is a rate summed across replicas. Load the updated rule file if you maintain your own copy of the shipped alerts. See Upgrading. -
Gateway heartbeat batches now report their latency and bound how long they wait on a failing presence store (#5686). A new histogram,
yuzu_server_gateway_batch_heartbeat_duration_seconds{outcome="ok"|"degraded"}, records how long the server takes to handle each gatewayBatchHeartbeat;degradedmarks a batch in which the session directory read, the lease renewal or a presence write hit a Postgres problem. When the agent presence store fails during a batch (for example a saturated connection pool), later entries in that batch no longer each wait up to 250 ms for a connection: their presence write is skipped, except for probes (the entry right after the first failure is attempted and the spacing doubles on each further consecutive failure), so under total saturation (every attempt fails) a batch of N makes at most aboutfloor(log2 N)+1attempts, each waiting at most its 250 ms pool acquire (not once, and not N times; an attempt stalled on a row lock is bounded by the poollock_timeoutinstead); under partial brownout a success resets the spacing, so waits can exceed that count (a strictly alternating failure pattern can still approach about N/2 failed waits, about 41 s at N=330, still half of the old N). Skipped writes are counted in the newyuzu_server_agent_presence_upsert_skipped_total, with one rate-limited warning;yuzu_server_agent_presence_store_failed_total{op="upsert"}now counts the attempts actually made (aboutfloor(log2 N)+1at most per saturated gateway batch, not one per entry), so per-entry rate queries on it under-report during saturation. Those heartbeats are still acknowledged and their route leases still renewed, and once the presence store recovers (a later probe in the same batch or the next batch then refreshes the row) the presence row is current again. Limit: during a long presence brownout, other replicas' scope evaluation can be narrower while rows are unrefreshed (rows age out after 90 s by default). Direct (non-gateway) heartbeats are unchanged. No alert ships for it; seedocs/user-manual/metrics.md. -
Linux ELF floor gate (report-only).
scripts/ci/check-elf-floor.shmeasures the versioned-symbol requirements of every ELF in the Linux release staging directory against the committed floor filesdeploy/packaging/linux-floor-{agent,server}.json(agent: glibc 2.28; server and gateway: glibc 2.34): glibc, libstdc++, libstdc++ ABI, systemd, libblkid and OpenSSL tags are compared with the floor, other families (for exampleGCC_*from libgcc) are only reported, and forbidden symbols (EVP_sm4_*) and non-$ORIGINRUNPATHentries are flagged. The release workflow runs it report-only on the Linux x64 staging directory and uploads the report, anddeploy/packaging/linux-floor-baseline.jsonrecords the measured gap of the v0.14.1 artifacts; nothing fails because of it yet (ADR-2003 (Accepted, PR #5631), slice S0). This changes no shipped package and no minimum supported distribution: the floor files state a target, and the packages still require glibc 2.38 until the build moves to the floor in a later slice (#5143 and #5171 remain open). Pre-release QA also gains non-gatingrockylinux:8andamazonlinux:2023.rpminstall legs, expected to fail until then. -
Hosts found by network discovery can now be read through a bounded REST route, an MCP tool and a dashboard fragment.
GET /api/v1/network/discovered-devices(alsolist_discovered_devicesover MCP andGET /fragments/network/discovered) lists discovered hosts newest last-seen first, capped at 500 rows, with host and subnet counts over the whole filter and atruncatedflag, and takes an optional exactsubnetfilter. It needsInfrastructure:Read, refuses service-scoped API tokens, and is audited asnetwork.discovered.view(the REST route refuses to serve if that audit row cannot be written). A store fault is reported as unavailable, never as an empty list, and no managed flag is returned because discovery does not yet match hosts to enrolled agents. The olderGET /api/discovery/resultsis unchanged. The read is gated by the flatInfrastructure:Read, so an operator confined to a management group is refused with403rather than shown a narrowed list. The dashboard fragment answers an invalid filter or an unavailable store with a failure box and HTTP 200, so htmx shows it. -
Read-only deployment list and detail over REST and MCP, and an NVD status card.
GET /api/v1/deployments,GET /api/v1/deployments/{id}and MCPlist_deployments/get_deploymentshow your deployments from the live/autopipeline without advancing the engine or writing a per-poll audit row. A deployment reads Running or Complete, with the failed or skipped count on a finished one ("Complete, 2 failed"); the installer URL and arguments are never returned. Store faults answer503rather than an empty list: a degraded store with a retry hint, a store that was never configured without one. The dashboard fragmentsGET /fragments/auto/deploymentsandGET /fragments/auto/deployments/detail?dep=render the same data.GET /fragments/auto/nvdshows the NVD feed status (CVE count, last sync, running, last error). The deployment list reportstruncated(inpaginationfor REST, at the top level for MCP) instead of a total, so a client can tell a short page from the whole list;limitmust be a whole integer, and20junkis400. Deployments with the same start time list in a fixed order (highest id first), so the cut at the limit does not flip between reads. On the dashboard fragment a list cut at 50 says it shows only the newest ones, and alimitthat is not a whole integer falls back to 50.GET /api/v1/deployments/previewalso keeps the A4 401 body. Devices inside a deployment are ordered by hostname, then agent id. MCPget_deploymentrefuses an id longer than 64 characters or one that is not lowercase hexadecimal, and publishes a typed result. -
Settings > Data and retention page (read-only).
GET /settings/data(admin only) shows the instruction-response, audit-log and Guardian-event windows the server is actually enforcing, flags a stored value that is not in force, and adds a JSON-to-CSV converter overPOST /api/export/json-to-csv.GET /api/v1/settings/data-retentiongainsguardian_event_retention_daysand anenforcedobject (null when a store is not open). Docs now state thatPUT /api/config/<retention key>is stored but not enforced (#483). -
Settings > Developer and API page (read-only).
GET /settings/developer(admin only; so are its five fragments, which also takeInfrastructure:Read) shows the OpenAPI document link with a live operation count, the REST route catalogue with a filter, the permission catalogue with the roles that allow each pair, and the build-embedded plugin manifests with per-OS support. It reads the existing discovery builders in process, so it adds no REST route or MCP tool; the five new fragments are ledgered against their/api/v1/discover/*twins. -
Settings gains a General page.
/settings/generalgroups the Appearance preference (table
density, saved in this browser), the agent heartbeat timeout and log level (the two runtime
settings that already had aPUT /api/config/{key}route and no UI), the existing
server-configuration and analytics read-outs, and a recent-events table with Pending and Emitted
counts. Changing the heartbeat timeout, or lowering the log level towarnorerror, asks for
confirmation first. The page is admin-only and additive: every panel stays on/settings, and no
REST route or MCP tool is added. The server-configuration table now labels the old "Session
Timeout" row "Agent heartbeat timeout" (it is the agent liveness window, not the dashboard
sign-in expiry) and gains read-only "Dashboard idle timeout" and "Dashboard session lifetime"
rows;GET /api/v1/settings/server-configgainssession_inactivity_secondsand
session_absolute_lifetime_seconds. -
yuzu-server --healthcheckprobes a TLS-enabled server from inside the image. Exits 0 only when the configured listener answersGET /readyz(or--healthcheck-path /livez) with HTTP 200; the certificate chain is verified only against the install CA (default-ca.pem) or, with--https-cert, against the first certificate in that file (the server certificate itself; certificates after it are not trusted, and a first certificate that is not a-----BEGIN CERTIFICATE-----block is refused, never the whole file; the anchor must be a regular file of at most 64 KiB, and a larger, missing or unreadable one is refused the same way) — never the system trust store; hostname is not checked, redirects are not followed, connect is bounded at 1 s, each TLS handshake step at 1 s, the request as a whole at 3 s, and the whole probe at 4 s. On failure one stderr line names the target, the status or TLS reason, and the trust anchor. Reads the server's own listener flags and environment, neveryuzu-server.cfg. Images built before this flag exit on the unknown argument, so keep their existing probes. (#5906) -
Sync now on the Software page. A new
POST /api/v1/hardware/syncasks one device, a management group, a scope expression or the whole fleet to re-send its installed-software inventory now. It needs a globalExecution:Executegrant (an operator limited to management groups is refused), contacts only agents attached to the server process that handles the request (skipped_offlinecounts devices that server did not reach: no live session there, including a device attached to another server, or a refused send; a device that is offline everywhere is not enumerated for fleet and scope runs, so the counts describe what that server could reach, not the whole fleet), skips agents older than 0.13.1, refuses more than 500 reachable devices, allows one multi-device run per operator per 60 seconds and one in flight per server process, and writes theinventory.sync.requestaudit row before anything is sent. The Software page gets a Sync now button and dialog for the three scopes. The MCP toolsync_hardware_softwaremakes the same request and shares the cooldown and the one-run-at-a-time limit, so neither can be avoided by switching surface. A browser (session-cookie) request must carry a same-siteOriginorReferer(403otherwise); API and MCP token callers are exempt. Who is reached is decided at send time by the same dispatch seam as every other operator dispatch, and a run that stalls on one agent no longer blocks other runs indefinitely (the in-flight slot is a two-minute lease). -
Colour schemes. Settings > General > Appearance now offers Slate, Navy and Jade (dark) and Light, Light Baby Pink and Light Hot Pink (light). The choice is stored in this browser and applied before the page paints.
-
The agent-count previews now name a filter whose value is too long to be indexed.
GET /api/v1/management-groups/agent-count-previewand the MCP toolpreview_management_group_agent_countreturn a new field,unindexed_filter_keys: the mangled keys (never the values) of any filter whose value is longer than 2048 bytes as ingest stores it (a NUL or invalid UTF-8 byte counts as 3), an empty array when none is. Such a value is not indexed in newly stored responses, so when the field is not emptyagent_countcan be 0 or an undercount. The field is always present and additive;agent_countand every status code are unchanged (#5623). -
The gateway management plane can admit the server by its CA-issued identity instead of an SPKI pin, as an opt-in (#6016, ADR-2004). New gateway settings:
mgmt_peer_auth_mode(pin,identity_or_pinoridentity; an unset value is alwayspin, so nothing changes until you set it),mgmt_peer_identity(optional; otherwise derived at boot from a management listener'scacertfilewhen that file holds exactly one certificate) andmgmt_peer_denied, a list of{serial, Hex}certificate serial numbers refused in every mode (keyed on the serial, never the key, because the server re-issues its leaf over the same key). Once you opt in, a server key renewal that keeps the identity needs no change on a gateway that has no pin configured (or runsidentity_or_pin) and has no refusal window; inidentitymode a configured pin must also match. New refusal reasonsno_uri_san,identity_mismatch,identity_unresolvedandrevokedonyuzu_gw_mgmt_auth_rejected_total(inidentity_or_pinwith pins configured a peer that fails both paths because it has no identity is counted with the pin's reason, whileidentity_unresolvedandrevokedare always counted as themselves, and a pin that never resolved is reported even while the identity admits the traffic); new counteryuzu_gw_mgmt_auth_admitted_total{method="identity"|"pin"}; new alertYuzuServerIdentityUriAbsent(the built-in server certificate lacks the identity). An identity over acacertfilethat is not demonstrably exactly one readable certificate (more than one, or empty, unreadable or half-written) refuses boot (identity_over_ca_bundle) unless the mode isidentityand a pin is configured. See "Authorizing the server by its identity" in the gateway manual. -
/cryptodashboard: fleet cryptographic-library inventory. A new Posture > Crypto page shows which cryptographic libraries (OpenSSL, LibreSSL, BoringSSL, GnuTLS, NSS, Mbed TLS, wolfSSL, Libgcrypt, libsodium, Nettle, Botan, Bouncy Castle) are installed across the fleet, with the number of distinct devices per family, a per-version breakdown and a device drill-down. It reads the existing daily installed-software inventory and matches package names against a curated, versioned table (table_versionin every response); no agent change and nothing new is collected. The evidence is package-level only: statically linked, vendored and OS-bundled copies are not visible, Windows installer builds of OpenSSL are not matched (no real installer DisplayName has been recorded to base a rule on), and an empty result means "no matching package rows in the caller's visible inventory", not that a library is absent. Yuzu reports presence and versions and does not judge them. RequiresInventory:Read; management-group-confined and service-scoped callers see counts over their visible devices only (service-scoped tokens are refused with 403 until RBAC enforcement is on). The same data is available asGET /api/v1/crypto/librariesandGET /api/v1/crypto/libraries/devicesand as the MCP toolsget_crypto_librariesandlist_crypto_library_devices. Every read is audited (crypto.libraries.view,crypto.libraries.devices, set-and-proceed: when an audit row is lost the response says so, and the page shows an "Audit not recorded" notice), and a degraded inventory store is a503withretry_after_ms, never an empty list. Responses say where the package names came from (names_sourcecatalogueorlive, andnames_as_of); a device-drill cursor records that source: a page whose cursor was issued on live names is refused with a400once the catalogue is in use (this can recur if the catalogue goes stale and recovers mid-walk), while a newer catalogue refresh or a switch to live names continues. A version bucket that lists more than 8 package names setspackage_names_truncated, and when the store's row ceiling fires every family is flaggedversions_truncated. The MCP tools reject a non-objectargumentsvalue and anecosystemoutside the published list. Each server process runs at most two/cryptostore reads at once; a read over that bound is refused at once with a retryable503. Seedocs/user-manual/crypto.md.
… changelog truncated — this release's changelog exceeds GitHub's 125,000-character release body limit.
See CHANGELOG.md for the full list of changes.
Enterprise installers
| Component | Windows | macOS | Debian/Ubuntu | RHEL/Fedora |
|---|---|---|---|---|
| Agent | .exe (InnoSetup) |
.pkg (launchd) |
.deb (systemd) |
.rpm (systemd) |
| Server | .exe (InnoSetup) |
— | .deb (systemd) |
.rpm (systemd) |
| Gateway | — | — | .deb (systemd) |
.rpm (systemd) |
Windows silent install (GPO / SCCM / Intune)
# Agent
YuzuAgentSetup-*.exe /VERYSILENT /SUPPRESSMSGBOXES /SERVER=yuzu-server:50051 /TOKEN=<enrollment-token>
# Server
YuzuServerSetup-*.exe /VERYSILENT /SUPPRESSMSGBOXES /ADMIN_USER=admin /ADMIN_PASS=<password> `
/HTTPS_CERT=cert.pem /HTTPS_KEY=key.pemSilent uninstall — agent: "C:\Program Files\Yuzu\unins000.exe" /VERYSILENT /SUPPRESSMSGBOXES; server: "C:\Program Files\Yuzu Server\unins000.exe" /VERYSILENT /SUPPRESSMSGBOXES (or the path in the product's UninstallString registry value, if it was installed to another directory). Each removes the program but keeps its data directory: %ProgramData%\Yuzu (the agent's identity and mTLS private key) or %ProgramData%\Yuzu Server (databases, configuration, TLS keys). To decommission a device, revoke its certificate on the server and delete that directory (see docs/agent-bundle.md). Uninstallers from earlier releases (up to 0.14.0-rc2) hang on a silent uninstall; upgrade first.
macOS silent install (Jamf / MDM)
sudo installer -pkg YuzuAgent-*.pkg -target /Linux install
# Debian/Ubuntu
sudo dpkg -i yuzu-agent_*.deb
sudo systemctl edit yuzu-agent # set --server address
# RHEL/Fedora
sudo rpm -i yuzu-agent-*.rpmPlatform archives
| Archive | Platform | Arch |
|---|---|---|
yuzu-linux-x64.tar.gz |
Linux (server + agent) | x86_64 |
yuzu-linux-arm64.tar.gz |
Linux (server + agent) | ARM64 (coming soon) |
yuzu-gateway-linux-x64.tar.gz |
Linux (gateway) | x86_64 |
yuzu-windows-x64.zip |
Windows (server + agent) | x86_64 |
yuzu-macos-arm64.tar.gz |
macOS (server + agent) | ARM64 (Apple Silicon) |
Compose Wizard
yuzu-compose-wizard-*.zip — browser-based step-by-step
generator for a customised docker-compose.yml + .env.
Zero dependencies — extract the zip and open index.html in
a browser. Contributed by
@fjarvis in
#405.
unzip yuzu-compose-wizard-*.zip
cd yuzu-compose-wizard-*
# Open index.html in your browser, or:
python3 -m http.server 8000Docker images
docker pull ghcr.io/DevNullLtd/yuzu-server:v0.15.0
docker pull ghcr.io/DevNullLtd/yuzu-gateway:v0.15.0Architecture: linux/amd64. Images are published to GHCR by this workflow as part of the release.
Verify checksums
sha256sum -c SHA256SUMSVerify signature (cosign)
cosign verify-blob --bundle SHA256SUMS.sigstore \
--certificate-identity-regexp '.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
SHA256SUMSSupply chain: SBOM, SLSA provenance, signed images
Every release ships CycloneDX + SPDX SBOMs per platform
(yuzu-*-{linux-x64,windows-x64,macos-arm64}.{cdx,spdx}.json,
yuzu-{server,gateway,postgres}-image.{cdx,spdx}.json and
yuzu-{server,gateway,agent}-chisel-image.{cdx,spdx}.json), GitHub-native
SLSA build provenance attestations for every binary archive
and Docker image, and cosign keyless signatures on both the
Docker images and the SHA256SUMS manifest. See
docs/user-manual/release-verification.md
for the full verification workflow (gh attestation verify,
cosign verify, CycloneDX / SPDX validation).
See the user manual for full setup instructions.