Skip to content

Yuzu v0.15.0

Latest

Choose a tag to compare

@github-actions github-actions released this 30 Oct 16:45
7937ede

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.config are 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 /healthz and /readyz with 200, so roll the image and config back together too.
  • The server now refuses to start with --gateway-upstream unless a gateway peer is authorised. The generated default certificates are pinned automatically. With your own certificates, configure --gateway-peer-pin or --gateway-peer-pin-file (env YUZU_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) need Forensics:Read as well as Response:Read (#4990).
  • With RBAC off (the default), the audit trail is admin-only (#5771): GET /api/audit, GET /api/v1/audit, MCP query_audit_log and the yuzu://audit/recent resource.
  • Result-set members need Infrastructure:Read and are limited to devices the owner can still see. With RBAC enforcement on, only Administrator and ITServiceOwner hold it by default, so owners with other roles get 403 on their own sets until it is granted. The default RBAC-off configuration is unaffected.
  • GET /api/config, GET /api/v1/config and get_runtime_config show 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 Origin or Referer. 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.cfg no 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_USER and /OPERATOR_PASS are 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 operationId is unique. Generated clients and byte-level pins change.
  • registry plugin 2.x: registry links are refused instead of followed, so a write under HKLM\SYSTEM\CurrentControlSet now fails; write under ControlSet001 instead.
  • GET /login is a 302 to /home, and unauthenticated page requests go to /home.
  • Source builds: the Erlang gateway needs OTP 28.4 or any later 28.x. Otherwise meson setup skips the gateway with a warning (-Drequire_gateway=true makes it an error).
  • --max-agents / YUZU_MAX_AGENTS is 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 registry writes (#6484). The server does not check minAgentVersion at dispatch. A registry set_value using the 2.x data_hex or the new value types, sent to an agent still on registry 1.x, writes an empty or mistyped value and reports status|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 sudoers action. Windows reads password_policy, lockout_policy and audit_policy from one read-only secedit /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 under agent.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 reports secedit:export_incomplete, never rows. sudoers (Linux and macOS) reads /etc/sudoers and /etc/sudoers.d/*, lexed as sudo 1.9.16 lexes them, and reports one row per rule clause; an unreadable file is permission_denied/unreadable, never an empty result, and Windows reports a fixed windows_has_no_sudoers row. The action is Security read-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 explicit unsupported row), read-only and in-process (never a sqlite3 CLI shellout), over an immutable descriptor with bounded schema/row/value/byte/time reads; it replaces the single unsupported/macos:planned placeholder row released in 0.14.0. The plugin is still off until an operator enables it. An agent identity without Full Disk Access reads denied for every source and reports PERMISSION_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 own TCC.db records, not what tccd enforces. Windows ships in the same release (see the privacy_permissions Windows leg entry).

  • privacy_permissions Windows leg. Reads each real profile's CapabilityAccessManager ConsentStore (the loaded HKU hive first, else the profile's NTUSER.DAT under SeBackup/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 the unsupported/windows:planned placeholder row listed under 0.14.0. The offline NTUSER.DAT load 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 still AdminOrApproval, single-target and audited). An offline mount alters the logged-off profile's hive sequence number and hash.

  • The filesystem plugin's get_acl action 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_access and, 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 a control row, 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. Windows ace rows now carry a fifth field with the inheritance flags, an undecoded ACE type prints - for its mask instead of a zero, and a failed GetAce or GetAclInformation now ends the command with an error instead of silently returning fewer rows.

  • New read-only mgmt_posture agent plugin — which management plane a device is under, on Linux and macOS (the Windows leg is not read yet). The single posture action reports facts only and changes nothing. On Linux it reads /etc/sssd/sssd.conf and /etc/sssd/conf.d/*.conf for the active SSSD domains and reports the plane as ad, ipa or none (an IPA realm in /etc/ipa/default.conf also reports ipa), plus whether /etc/krb5.keytab is present; it reports the configuration as written, not the live join state. On macOS it reads MDM enrolment and the MDM server host from profiles status -type enrollment. An SSSD configuration the agent is not allowed to read reports permission_denied with plane unknown, 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 one unsupported status row (windows:planned) until it ships.

  • network_config routes action. 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 other network_config actions; the result definition is device.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_catalogue and get_software_versions share the REST catalogue's model. GET /api/v1/inventory/software and query_installed_software gain a version filter, a source field per row and opaque cursor/next_cursor pagination; the REST route also gains q, kind, ecosystem and source filters and a format=csv|json export of up to 10000 rows (REST-only; a CSV export carries its continuation in an X-Next-Cursor header and ends with a # result_truncated_by_cap record when more rows may exist).

  • Software catalogue REST routes. GET /api/v1/inventory/software/catalogue returns the fleet software title catalogue with totals that cover every row under the active filters, first-page KPIs (headline numbers plus reporting/connected/stale device counts) and a format=csv titles export; GET /api/v1/inventory/software/catalogue/versions returns installs per version. Keyset cursors are bound to the filter set and to the hourly catalogue refresh (a stale or mismatched cursor is a 400), 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 retryable 503, 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/apps and list_dex_apps serve 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 below kDexCohortFloor devices (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) and GET /api/v1/discover/instructions carry input_schema per definition: the canonical JSON Schema that POST /api/instructions/{id}/execute enforces (or null), alongside input_schema_error (null, parameter_schema_not_canonicalisable, or input_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_schema is unchanged. A non-null input_schema can 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 job definition-descriptions.

  • DEX app performance: per-version crash and hang join. GET /api/v1/dex/perf/app and the MCP tool get_dex_app_perf now carry a versions[] 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 answers 503 when either underlying read degrades, so a caller that reads only points[] 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 the GET, PUT and DELETE /api/v1/dex/score-policy entries of this release read it.

  • New read-only shares agent plugin — what a host is configured to serve over SMB and NFS, on Linux (the macOS leg is not read yet). The single list action 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/*.exports and /etc/samba/smb.conf. It reports the configuration as written, not live nfsd or smbd state, 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 by Response:Read holders, as for every plugin. The macOS leg reports one unsupported status row (macos:planned) until it ships.

  • Workflow run list: GET /api/v1/workflow-executions and MCP list_workflow_executions. The first way to list workflow runs (only fetch-by-id existed). Newest-first keyset paging (cursor / next_cursor), filters workflow_id, status, since, until, limit, and latest_per_workflow=true for 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_ids and step results stay on the detail route. Gated on Workflow:Read through 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 toward pagination.total. Input is strict (unknown or repeated parameters are 400), a store failure or the 1.5 s statement timeout is a 503 with retry_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:Read and writes the same rbac.assignments.list audit 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 as GET /api/v1/audit and 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 as audit.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 with AccessReview:Attest can open a review, attest or flag a grant and close a campaign from the page, through the existing /api/v1/access-reviews routes. 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, and access_review.exported with format=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/audit now accepts target_type, target_id, since, until and offset, returns each row's id, and reports the real start and page_size in pagination. The MCP query_audit_log tool gains target_id and offset. target_id requires target_type; malformed or out-of-range values are rejected with an error rather than ignored. Rows with equal timestamps are now ordered by id, 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/apps and the MCP tool list_dex_apps now 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) and apps_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 as offline_affected while any affected device is offline (the common case for multi-device applications on a fleet with laptops). A failed read now answers 503 (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 and GET /api/v1/dex/app still count every platform until the tab is rewired. Additive otherwise.

  • Fleet roster and per-policy compliance counts computed in SQL. OfflineEndpointStore::fleet_state_counts returns the roster total, heartbeat-stale count and a per-platform split (Windows, Linux, macOS, Other) from one COUNT(*) ... 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 as ComplianceApi::compliance_by_policy) returns every policy's compliance counts from one cached GROUP BY policy_id, status read in place of one query per policy, and AgentRegistry::known_agent_count counts the online set without building the id list. fleet_state_counts also returns an exact SQL live presence count that stays correct past the 100,000-row live-id cap, where known_agent_count reports unavailable. The failure counter yuzu_server_agent_presence_store_failed_total gains an op="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_inventory gains a read-only store_packages action and real Linux legs. managers on 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_packages lists 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 kind unknown, and a flatpak whose active link cannot be read with state unknown, rather than failing the action. The daily installed-software inventory collects the active rows as ecosystems snap and flatpak (flatpak version is empty: a branch is a ref, not a version), and a constrained managers answer that still names a present manager no longer skips the whole cycle. macOS reports store_packages unsupported by design (installed_apps lists applications); Windows is planned. Also carries #4881's descriptor and wording corrections (unsupported legs declared rung 0, the Linux roster owner named as installed_apps.list_inventory).

  • Settings gains an Integrations page. /settings/integrations hosts 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 with GET /api/v1/ca/issued?offset= and revoked with POST /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, same Security:Read gate as GET /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 MCP list_issued_certs tool now carry crl_version, crl_published_at, crl_next_update, crl_revoked_count and crl_stale. Each is null when it could not be determined (a CRL published before counts were recorded has a null count, never 0), 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/tokens and POST /api/v1/upload-grants now send Cache-Control: no-store on 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/tokens entry 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 need EnginePrincipal:Read (the same permission as GET /api/v1/engine-principals; administrators only while RBAC is off), the revoked principal's free-text detail also needs AuditLog:Read, and the check runs only when the caller holds AuditLog: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/list and /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/notifications routes 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 a 401 or 403. Notification text is placed with textContent only. 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/apps and list_dex_apps now carry a policy block (source default|custom, revision, the four tier edges and names, and a fault reason when a stored document is invalid and the shipped defaults are in force) and a per-application profile marker; the policy lives under the runtime-config key dex_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 a 503 ending (score_policy_unavailable) instead of scoring under a guess. GET /api/config lists dex_score_policy under a new read_only_keys field, and the generic PUT /api/config/dex_score_policy is refused with a 400 naming the policy route; the key is written through PUT and DELETE /api/v1/dex/score-policy, and until a policy is written every response reports source: "default".

  • msi_packages gains a read-only list_inventory action 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 (kind pkg, package_id set 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 existing list and product_codes actions are unchanged apart from an exception guard; macOS and Linux report the action unsupported.

  • New read-only remote_access_audit agent plugin — who can log in to a host over SSH, on Linux and macOS. The single list action reports the sshd_config values 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 the authorized_keys of the homes it walks (/home and /root on Linux; /Users owned by uid 500 or above and /var/root on macOS), and whether the sshd service 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. Where sshd_config sets a login source the plugin does not read (a certificate authority, a key command, a principals command or file, host-based authentication or AuthenticationMethods) 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 by Response:Read holders, as for every plugin. The plugin is Forensics-class: it ships off behind the plugin-config kill switch and is gated at Forensics:Read with approval, one device at a time. The Windows leg is a planned placeholder that reports one unsupported status 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-anchor beside the server data. Keep that file with your backups: it is not in the pg_dump, scripts/yuzu-backup.sh and scripts/yuzu-restore.sh now copy it, and a restore without it is reported as unwitnessed. /readyz carries an enablement object and /health an enablement_regressed flag; neither gates readiness. New yuzu_server_enablement_* metrics and a yuzu-enablement alert group ship with it; until the later change that issues desired state, only YuzuEnablementRegressed and YuzuEnablementAnchorWriteFailing can 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 mints registry.write audit rows from a registry plugin's audit| 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 the audit| lines a registry command streams from its mutating actions (set_value, delete_value, delete_key, set_user_value, delete_user_value, apply) into registry.write audit rows carrying the old and new values, principal=agent:<agent_id> and the command_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 be registry (matched ASCII-case-insensitively), and the command_id is 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 each audit| 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 by yuzu_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 moves yuzu_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 in docs/user-manual/upgrading.md is about 130 seconds, up from about 115. registry 2.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 each audit| line is the agent's own report; the server checks the shape of the command_id and the stamped action and takes the plugin from its dispatch record. See docs/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 in yuzu.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.

  • registry 2.0.0: typed, verified writes on a link-checked handle. set_value now writes all six value types (REG_SZ, REG_EXPAND_SZ, REG_BINARY, REG_DWORD, REG_MULTI_SZ, REG_QWORD) from text or exact data_hex bytes, 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: on HKLM and HKU one 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), on HKCU and HKCR, for a missing key and whenever the open cannot decide, the walk itself, which refuses a link (key_is_link), creates missing components, refuses concurrent_change and 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's created= reports every key left on a returning exit). Keys are deleted by handle with NtDeleteKey, so a swapped path cannot redirect a write or a delete; delete_key still removes only a key with no subkeys and now reports counted_keys, counted_values, dry_run and capped (the count stops at 10,000 nodes: the key plus up to 9,999 values). Every mutating action emits one audit| 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 by AuditLog:Read and Response:Read holders (Viewer, Operator, PlatformEngineer, ITServiceOwner and Administrator), the legacy /events stream (any authenticated non-service-scoped session) and, with the write parameters value and data_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 a registry.write row 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.

  • /swagger API reference page in the dashboard. It renders the OpenAPI document served at /api/v1/openapi.json as 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-level tags with an x-yuzu-guide extension. The page takes the same Infrastructure:Read gate 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 an x-yuzu-unavailable extension on the operation. GET /api/v1/openapi.json leaves the guides out by default and includes them with ?guides=true, so the MCP resource yuzu://openapi and 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 kEchoTimelineUi is 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 of GET /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/me and GET /api/me gain principal_kind (human or engine), auth_source (always present), mcp_tier, scope_service, rbac_roles and rbac_roles_resolved. get_me refuses service-scoped tokens; the REST route still lets them describe themselves. Refs #2146.

  • Echo registry source ($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_pid is 0); the "Echo registry source 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 through echo.sql. Retention is 500 000 rows; opt out with echo.configure registry_enabled=false. Opting out, and rolling back to an older agent, both leave the existing rows and the unmasked per-key baselines in echo.db until echo.purge_source is run on the paused source; run it before a rollback, since an older agent cannot. Device pages show the new source under a System category.

  • Echo retention tiers on the data-retention API. GET /api/v1/settings/data-retention now carries echo.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 registry source names the process that wrote a change. $Registry_Live rows now carry actor_pid and actor_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. detail says how the actor was reached: attr:corr when exactly one process wrote a matching value, and, with no process named, attr:ambiguous:<n>, attr:none, attr:degraded (write events were lost, with a gap:loss:<n> row), attr:off or attr: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 say attr: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 through echo.sql). echo.status gains registry_etw_events, registry_etw_dropped, registry_etw_lost, registry_attr_attributed, registry_attr_ambiguous and registry_attr_none, and registry_capture_method reads notify+etw while 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.

  • registry 2.1.0: per-user hive writes and a declarative apply with a returned undo manifest. set_user_value and delete_user_value write or delete one value in a user's hive, resolving the profile as get_user_value does, except that a write never resolves a <SID>.bak leftover: through the live HKEY_USERS\<SID> hive when the user is logged in, otherwise through the offline NTUSER.DAT behind the shared hive-file guard, under the offline-hive lock, with the same typed, read-back-verified write as set_value and a RegFlushKey before 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 names hive=HKU and the profile's sid. apply runs a plan of up to 256 HKLM set_value / delete_value / create_key / delete_key items: 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, with undo_complete in 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 own registry.write row; items skipped after a stop have none. apply and delete_user_value are Destructive, so they need explicit agent_ids and refuse scope and broadcast targeting. Three new definitions: windows.registry.set_user_value, windows.registry.delete_user_value and windows.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). hardware declares chassis, baseboard, swap and machine_id; disk_actions declares partitions and topology. Every new action answers one typed status|<action>|unsupported|<os>:planned row 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 /groups now 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 on yuzu_server_mgmt_group_read_degrade_total wherever 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_software gains package_id and source (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-insensitive q filter over name, publisher, ecosystem and source, exact kind/ecosystem/source filters 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_id and source; 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 rewrites installed_software under 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 from pkg_inventory (packages/managers, ecosystem brew) and every Windows optional feature, enabled or disabled, from windows_optional_features (list, ecosystem optional_feature, the DISM state in the version column). Every row now carries source, the producing plugin action; package_id stays empty for these producers and the reserved install_location/uninstall_string slots are still sent empty. An action that is not loaded or not supported on the OS is skipped (except installed_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 /software catalogue 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 (whose rate() is the duty cycle), yuzu_gw_upstream_lane_saturated_seconds_total{lane}, yuzu_gw_upstream_backpressure_duty_cycle (last 60 s) and yuzu_gw_upstream_slow (1 at a duty cycle of 0.5 or more). The new warning alert YuzuGatewayUpstreamSlow fires when the flag has been 1 for 5 minutes. The breaker still counts only failed calls (#5681). /readyz no longer waits for the upstream process: its circuit_breaker check reads the breaker state that process publishes, so a slow server no longer turns /readyz 503 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 as 200 notes (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 with Security:Execute quarantine or release a device from a
    dialog. Release ends the server record only and says so. The panel and list pills make one Security:Read list-gate decision per view and apply its
    scope in the store query, the same rule as GET /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 validated lens so 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_secret and grant_id in the History list. POST /api/dashboard/execute accepts an optional scope_expression field to target a send at an expression; an attribute the resolver does not know is refused with scope_unknown_attribute, because != and NOT would 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}/members and GET /fragments/result-sets/producers, answer refusals and store failures as 200 notes (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/app and the get_dex_app_perf MCP 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-zero stat_failures fault 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/summary fills 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 hold AuditLog:Read; detail, source address, user agent and session id are
    never shown. Server health reuses the /api/health document, 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/instructions accepts name, plugin, type and limit (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 holds InstructionDefinition:Read the table gains a "Needs approval" column, and when the caller holds InstructionSet:Read it 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-sets now writes instruction_set.create rows for success, store failure, conflict and rejected input, and DELETE /api/instruction-sets/{id} writes an instruction_set.delete success row (a failed audit write sets Sec-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 behind InstructionDefinition: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 from GET /api/v1/instructions/{id} (so they render), shows grant_secret and grant_id as 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 / :Delete can 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's props.<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 holding Tag:Delete (previously any
    Tag:Write holder 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
    audited DELETE /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 the Decommission:Delete permission 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 hold InstructionDefinition: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 new GET /fragments/workflow-runs and GET /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 /workflows can 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 new GET /fragments/workflows and a view=page mode on GET /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 when SoftwareDeployment:Read is 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:Read sees a message naming the missing permission, and the tab is present even when no device reports network telemetry yet. /network#discovered opens it directly.

  • GET /api/v1/config, PUT /api/v1/config/{param} and the MCP tools get_runtime_config and set_runtime_config. The runtime-configuration API now has v1 REST and MCP twins of the legacy GET /api/config and PUT /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 the config.update audit rows (denied, or attempted then success or failure) 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, like set_runtime_config, answers an effect field (immediate, deferred or stored_only) because applied: true only means the value was stored: the retention windows, auto_approve_enabled, plugin_signing_required and the oidc_* keys other than oidc_admin_group and oidc_redirect_uri (which wait for a restart) are stored_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 a denied audit row (dex_score_policy answers invalid params).

  • Leader-election metrics and alerts (HA WS-11, #4014). Every server with a Postgres-backed leader
    elector now exports yuzu_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_seconds and
    yuzu_server_leader_loop_errors_total on /metrics, published at scrape time so a stalled coordination
    connection cannot stall a scrape. docs/prometheus/yuzu-alerts.yml gains the yuzu-ha-leader group:
    YuzuLeaderSplitBrain, YuzuLeaderNoLeader and YuzuLeaderCoordinationStale (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 on up == 0 for the
    server job (none ships). Replicas on different databases are not fenced against each other, which is the
    case YuzuLeaderSplitBrain can observe. The leader_elector: acquired leadership log line now carries the holder_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 ForensicsAnalyst RBAC role: a read-only way to delegate Forensics:Read (#4990). It holds exactly Forensics:Read, Response:Read, Execution:Read and Inventory:Read, with no Execute and 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/assignments or the MCP assign_rbac_role tool; 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 scoped Forensics:Read grant 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 runs pwpolicy -getaccountpolicies and reads /etc/security/audit_control. A refused read reports PERMISSION_DENIED/CONSTRAINED with a named token, never absent.

  • /test --instructions runner can drive Destructive and Forensics definitions at one chosen agent. scripts/test/instructions_runner.py gains --target-agent-id, --confirm-target (the agent's hostname, checked against GET /api/agents before anything is sent), --manifest and --allow-remote, and those classes now require the existing --params-override and --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 as dispatch_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 /test default) dispatches as before, but the runner is stricter for every run: each request carries a yuzu-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-id or a --dashboard with userinfo exits 2, --output refuses a symlink or a multi-link file, and SIGTERM stops the run like Ctrl-C. See /test SKILL.md, "--instructions mode".

  • 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-OS PUT as the plugin-level row (see Upgrading). The kill-switch REST routes and MCP tools accept an optional os (a present-but-blank ?os= on REST answers 400, and a non-string os on MCP is rejected as invalid params). A dispatch withheld this way answers 503 with reason: kill_switched_os, is recorded in the audit log as command.dispatch_withheld, and is counted on yuzu_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 transient 503 reason: os_gate_unreadable and retry_after_ms: 5000, is recorded as command.dispatch_withheld, and on the scheduled path is counted on yuzu_server_command_outbox_deliver_retry_cause_total{cause="os_gate_unreadable"}. Store schema plugin_config_store migration 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 tag yuzu.sync.<source>.skip_streak (today source="installed_software"). Read it alongside yuzu_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 its yuzu.sync.<source>.last_skip heartbeat 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/password with the current password — and an administrator can reset another local account's — Reset password, or POST /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 the admin role; with RBAC on, the caller needs a user Administrator grant. A JIT elevation, an IdP-group admin role or a custom role holding UserManagement:Write is refused 403 (durable_admin_required). Both routes accept interactive dashboard sessions only (API, MCP, engine and service-scoped tokens get 403), require a same-origin Origin/Referer and Content-Type: application/json, and enforce one password policy of 12–1024 bytes (UTF-8), now also applied to account creation; /login answers 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 same 403. 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_cleared is true only 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, or null with api_tokens_unknown when the token store cannot be read). The configured break-glass account cannot be reset (403 break_glass_target); under --auth-mode=sso-only only an armed break-glass account may change its own password. Audited as user.password_change / user.password_reset inside that transaction; if the row cannot be written nothing changes and the request fails 503 (audit_unavailable, Sec-Audit-Failed). SSO and SCIM accounts are refused 409. 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_PASS or /OPERATOR_PASS on 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 metric yuzu_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_spark is enabled, which no released build does, with one exception: yuzu.guardian_ack_maint_exceptions has 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_seconds and yuzu.guardian_compensation_pending_age_seconds (absent while nothing is pending, never a fabricated 0). The server exposes them as 13 yuzu_fleet_guardian_* fleet sums and two _max age gauges, and counts forged values in yuzu_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, the yuzu.guardian_generation tag, 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.yml now ships a per-replica rule that fires when yuzu_server_rbac_breaker_open has stayed at 1 on a replica for 5 minutes, meaning that replica's authorization reads keep failing and its fail-fast breaker has not closed. It complements YuzuRbacReadDegraded, 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 gateway BatchHeartbeat; degraded marks 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 about floor(log2 N)+1 attempts, 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 pool lock_timeout instead); 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 new yuzu_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 (about floor(log2 N)+1 at 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; see docs/user-manual/metrics.md.

  • Linux ELF floor gate (report-only). scripts/ci/check-elf-floor.sh measures the versioned-symbol requirements of every ELF in the Linux release staging directory against the committed floor files deploy/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 example GCC_* from libgcc) are only reported, and forbidden symbols (EVP_sm4_*) and non-$ORIGIN RUNPATH entries are flagged. The release workflow runs it report-only on the Linux x64 staging directory and uploads the report, and deploy/packaging/linux-floor-baseline.json records 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-gating rockylinux:8 and amazonlinux:2023 .rpm install 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 (also list_discovered_devices over MCP and GET /fragments/network/discovered) lists discovered hosts newest last-seen first, capped at 500 rows, with host and subnet counts over the whole filter and a truncated flag, and takes an optional exact subnet filter. It needs Infrastructure:Read, refuses service-scoped API tokens, and is audited as network.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 older GET /api/discovery/results is unchanged. The read is gated by the flat Infrastructure:Read, so an operator confined to a management group is refused with 403 rather 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 MCP list_deployments / get_deployment show your deployments from the live /auto pipeline 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 answer 503 rather than an empty list: a degraded store with a retry hint, a store that was never configured without one. The dashboard fragments GET /fragments/auto/deployments and GET /fragments/auto/deployments/detail?dep= render the same data. GET /fragments/auto/nvd shows the NVD feed status (CVE count, last sync, running, last error). The deployment list reports truncated (in pagination for REST, at the top level for MCP) instead of a total, so a client can tell a short page from the whole list; limit must be a whole integer, and 20junk is 400. 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 a limit that is not a whole integer falls back to 50. GET /api/v1/deployments/preview also keeps the A4 401 body. Devices inside a deployment are ordered by hostname, then agent id. MCP get_deployment refuses 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 over POST /api/export/json-to-csv. GET /api/v1/settings/data-retention gains guardian_event_retention_days and an enforced object (null when a store is not open). Docs now state that PUT /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 take Infrastructure: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/general groups the Appearance preference (table
    density, saved in this browser), the agent heartbeat timeout and log level (the two runtime
    settings that already had a PUT /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 to warn or error, 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-config gains session_inactivity_seconds and
    session_absolute_lifetime_seconds.

  • yuzu-server --healthcheck probes a TLS-enabled server from inside the image. Exits 0 only when the configured listener answers GET /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, never yuzu-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/sync asks one device, a management group, a scope expression or the whole fleet to re-send its installed-software inventory now. It needs a global Execution:Execute grant (an operator limited to management groups is refused), contacts only agents attached to the server process that handles the request (skipped_offline counts 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 the inventory.sync.request audit row before anything is sent. The Software page gets a Sync now button and dialog for the three scopes. The MCP tool sync_hardware_software makes 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-site Origin or Referer (403 otherwise); 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-preview and the MCP tool preview_management_group_agent_count return 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 empty agent_count can be 0 or an undercount. The field is always present and additive; agent_count and 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_pin or identity; an unset value is always pin, so nothing changes until you set it), mgmt_peer_identity (optional; otherwise derived at boot from a management listener's cacertfile when that file holds exactly one certificate) and mgmt_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 runs identity_or_pin) and has no refusal window; in identity mode a configured pin must also match. New refusal reasons no_uri_san, identity_mismatch, identity_unresolved and revoked on yuzu_gw_mgmt_auth_rejected_total (in identity_or_pin with pins configured a peer that fails both paths because it has no identity is counted with the pin's reason, while identity_unresolved and revoked are always counted as themselves, and a pin that never resolved is reported even while the identity admits the traffic); new counter yuzu_gw_mgmt_auth_admitted_total{method="identity"|"pin"}; new alert YuzuServerIdentityUriAbsent (the built-in server certificate lacks the identity). An identity over a cacertfile that 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 is identity and a pin is configured. See "Authorizing the server by its identity" in the gateway manual.

  • /crypto dashboard: 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_version in 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. Requires Inventory: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 as GET /api/v1/crypto/libraries and GET /api/v1/crypto/libraries/devices and as the MCP tools get_crypto_libraries and list_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 a 503 with retry_after_ms, never an empty list. Responses say where the package names came from (names_source catalogue or live, and names_as_of); a device-drill cursor records that source: a page whose cursor was issued on live names is refused with a 400 once 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 sets package_names_truncated, and when the store's row ceiling fires every family is flagged versions_truncated. The MCP tools reject a non-object arguments value and an ecosystem outside the published list. Each server process runs at most two /crypto store reads at once; a read over that bound is refused at once with a retryable 503. See docs/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.pem

Silent 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-*.rpm

Platform 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 8000

Docker images

docker pull ghcr.io/DevNullLtd/yuzu-server:v0.15.0
docker pull ghcr.io/DevNullLtd/yuzu-gateway:v0.15.0

Architecture: linux/amd64. Images are published to GHCR by this workflow as part of the release.

Verify checksums

sha256sum -c SHA256SUMS

Verify signature (cosign)

cosign verify-blob --bundle SHA256SUMS.sigstore \
  --certificate-identity-regexp '.*' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  SHA256SUMS

Supply 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.