Skip to content

Yuzu v0.13.0

Choose a tag to compare

@github-actions github-actions released this 11 Jul 14:43
· 15123 commits to main since this release

Warning

Known issue: the yuzu-gateway image crash-loops on recent Intel CPUs (#2150).
On AMX-capable Intel hosts (Sapphire Rapids and newer, including AWS c7i, m7i and r7i instances), the 0.13.0 gateway container exits at startup and keeps restarting. Its log shows sys_sigaltstack(): Internal error: Failed to set alternate signal stack, and docker ps shows Restarting (134). To check a host, run grep -m1 -o amx_tile /proc/cpuinfo: if it prints amx_tile, the host is affected.
The cause is that the image runs on Alpine 3.24 (musl 1.2.6), which rejects the signal stack size Erlang/OTP 28 requests on those CPUs. The agent, the server and the yuzu-gateway-chisel image are not affected.
Until you can move to an image carrying the fix (0.14.0, planned for 2026-10-10):

  • run the gateway on a host without AMX, or
  • for a single-node gateway only, use yuzu-gateway-chisel:0.13.0. Do not use the chisel image for a clustered gateway: it lacks the entrypoint that resolves the node's cluster address, so the node starts but never joins.

No 0.13.x release has the fix, so rolling back within 0.13.x does not help.


Added

  • Recurring instruction schedules now actually fire (#1191). ScheduleEngine::evaluate_due/advance_schedule had no production caller — schedules persisted and listed but never dispatched. A new ScheduleRunner poller (the PolicyEvaluator/PreflightRunner shape, 30s tick, joined before the stores) closes the gap: due schedules fire through the same tracked dispatch path as operator-initiated commands, the approval gate is never bypassed (requires_approval OR definition approval_mode != "auto", one ticket per occurrence, one-approval == one-run via the occurrence anchor), and every outcome is audited (instruction.schedule_fired, instruction.approval_required) and counted (yuzu_schedule_{fires,fire_failures,approvals_submitted,tick_errors}_total). Hardened against adversarial review before merge: POST /api/schedules requires BOTH Schedule:Write and Execution:Execute — a created schedule is a fleet-wide command-dispatch primitive with no further permission check at fire time — and re-enabling a disabled schedule requires Execution:Execute too (disabling never does, so a runaway schedule can always be stopped); delete and enable/disable are owner-scoped (created_by); one approval ticket is now scoped to the single schedule occurrence that requested it (approvals.schedule_id), so two schedules sharing the same (creator, definition, scope) can no longer settle on each other's ticket.

  • MCP agentic write surface + approval tickets (#289, R2). Five MCP write tools
    are now dispatched: set_tag, delete_tag, approve_request, reject_request,
    and quarantine_device (records the quarantine and dispatches live isolation).
    Approval-gated tools use a ticket-then-recall flow: the first call returns
    kApprovalRequired (-32006) with approval_id + status_url; after a second
    principal approves, the caller re-invokes with the approval_id and the ticket is
    consumed exactly once (args-bound, replay-rejected). Approval ids are CSPRNG-drawn;
    the mint is deduplicated so a token cannot flood the shared pending-approval queue.
    Review-round hardening (PR #1796): quarantine_device is Security:Execute on BOTH
    transports and the supervised approval-policy rule moved with it — closing a mismatch
    that let a supervised token quarantine via REST POST /api/v1/quarantine with no
    approval (the REST POST/DELETE routes now mirror-deny, #520); the three
    device-targeted write tools (set_tag/delete_tag/quarantine_device) authorize
    through the per-device management-group scope gate (require_scoped_permission), so
    a group-confined operator cannot tag or isolate out-of-scope devices;
    approved-but-unconsumed tickets expire after 7 days (a forgotten approval is no
    longer a forever-valid capability); the approvals store records consumed_by
    (SOC 2 CC7.2 evidence chain: submitted_by → reviewed_by → consumed_by); and the
    device-targeted ticket-mint audit rows carry agent_id= so SIEM can filter pending
    isolation requests by endpoint.

  • A2 discovery surface (Issue 17.1, #1794). GET /api/v1/discover/{permissions, instructions,routes,scope-kinds,plugins} plus five mirrored read-only MCP tools let
    an agentic worker enumerate RBAC permissions, instruction schemas, REST routes,
    scope-DSL kinds, and plugin actions from the live server (ETag + 304, A4-shaped
    degraded paths). REST and MCP share one builder per catalog (cannot drift).

  • Agentic-worker self-orientation (R2 follow-up). discover_plugins (REST + MCP,
    now catalog version: 2) enriches each action with its parameter_schema inline
    when the action has a published InstructionDefinition (joined on plugin+action), and
    reports actions_enriched_with_schema — so a worker learns how to call an action,
    not just that it exists, from one request. The execute_instruction and
    discover_plugins tool descriptions now spell out the async dispatch→poll pattern and
    point at the yuzu://operating-model resource. Measured effect: an LLM operator
    driving the fleet with no hand-fed action list made zero blind parameter guesses.

  • A4 error-envelope completion (R2). GET /api/v1/approvals/{id} (the approval
    status_url target); the auth_routes denial shapes are unified into one A4
    envelope carrying correlation_id and the missing securable_type:operation; the
    ~156 legacy error_json sites in rest_api_v1.cpp now emit the A4 envelope.
    Breaking — many of those /api/v1 errors were previously {"error":"<string>"}
    and are now the nested A4 object {"error":{"code","message","correlation_id",…}};
    a client that read error as a string will break. Migrate to error.code/
    error.message. See docs/user-manual/upgrading.md.

  • Installed-software inventory gains package-manager fields (blob contract v2, ADR-0016).
    Every row in GET /api/v1/inventory/software and the query_installed_software MCP tool
    now carries kind (package|app), ecosystem (rpm|deb|apk|pacman|windows|macos|homebrew),
    epoch, release, arch, signature_status (rpm only, from stored rpm header tags —
    never a live rpm -K verification), distro_id, and distro_version, alongside the
    original name/version/publisher/install_date. A field the ecosystem does not store is
    "", never synthesised — see docs/user-manual/inventory.md for the full per-ecosystem
    matrix. Collection is a new installed_apps plugin action, list_inventory; the
    operator-facing list/query/list_per_user actions are unchanged. New apk
    (Alpine) package enumeration on the sync path. Breaking — data shift on Linux
    rpm fleets:
    publisher now reflects the rpm PACKAGER tag (was VENDOR), and version
    is upstream-only with the release moved to its own column (was fused
    VERSION-RELEASE). A saved query/dashboard filter against the old publisher or
    fused version shape on rpm hosts will silently stop matching post-upgrade — see
    docs/user-manual/inventory.md before relying on either field in existing
    automation. No wire-protocol or gateway change (the fields ride inside the
    existing opaque sync blob). Upgrade note: deploy the server before agents; each
    agent's first post-upgrade sync triggers one expected full resend (need_full),
    phase-spread across the daily sync window, self-healing.

  • JIT elevation: OIDC amr-asserted MFA satisfies the second-factor
    requirement.
    An OIDC operator can now activate POST /api/v1/elevate
    when their SSO session was authenticated with IdP MFA — asserted via the
    OIDC amr claim and seeded into Session::mfa_verified_at at
    /auth/callback (the same mechanism OIDC login step-up already used).
    Closes the v1 limitation tracked in
    docs/security-reviews/jit-elevation-2026-06-30.md. Eligibility and MFA
    status are both keyed on a users table row, which OIDC login does not
    create — a federated-only identity needs one provisioned first (e.g.
    POST /api/v1/users). The mandatory-MFA gate branches EXPLICITLY on
    identity source: an OIDC session's only acceptable factor is a seeded
    proof (mfa_verified_at != epoch, never merely auth_source == "oidc",
    security-F1) with the toggle on, and it can never fall through to a
    local namesake account's TOTP enrollment (a hardening-round fix — the first
    cut's single-flag guard allowed exactly that fallthrough, mislabeling the
    audit trail). A single-factor (no-amr) SSO session is hard-denied with
    its own distinct reason; a stale seeded proof still falls through to the
    existing step-up freshness challenge rather than a silent grant. New toggle
    --jit-oidc-amr-elevation / YUZU_JIT_OIDC_AMR_ELEVATION (default true;
    --no-jit-oidc-amr-elevation disables JIT elevation for OIDC sessions
    entirely — they cannot fall back to a local TOTP step-up, since their
    step-up challenge is re-SSO, not a TOTP code). A one-time INFO log line
    announces the posture at boot when OIDC is configured. The
    role.elevation.granted audit detail records the factor source
    (duration_secs=<n> mfa=<oidc_amr|local_totp> expires_at=<rfc3339> justification=<text>
    — the machine-parsed mfa= and expires_at= tokens are placed before the
    free-text justification= field so a crafted justification can't forge either).

  • JIT admin elevation follow-ups: passive-lapse audit + absolute expires_at (SOC 2
    CC6.3/CC6.6).
    Closes the two residual risks tracked in
    docs/security-reviews/jit-elevation-2026-06-30.md. (A) A JIT elevation window that
    lapses PASSIVELY (no manual POST /api/v1/elevate/revoke) is now audited as
    role.elevation.expired — emitted LAZILY (no new background thread) by
    AuthManager::reap_expired_elevation, called from the AuthRoutes::resolve_session
    cookie chokepoint on the operator's first authenticated request after the window
    elapses; exactly-once, and a manual revoke never also produces a spurious expired
    row. (B) POST /api/v1/elevate's window is now clamped to the session's own absolute
    expiry (AuthManager::elevate_session), so an elevation can never outlive its cookie
    session; the response reports the TRUE remaining time as expires_in plus a new
    wall-clock expires_at (RFC3339 UTC), and the role.elevation.granted audit detail
    carries expires_at too.

  • SAML 2.0 SP — thin first slice. SP-initiated login against a single, statically-pinned IdP.
    HTTP-Redirect binding for the AuthnRequest; HTTP-POST binding at the Assertion Consumer Service
    (POST /saml/acs). Assertion signature is validated against the pinned IdP cert (in-document
    <KeyInfo> ignored); XML signature-wrapping attacks are defended; audience, recipient, and
    expiry are validated; InResponseTo is solicited-only and single-use (replay-protected). Sessions
    are ephemeral (auth_source="saml", role=user). Linux and macOS only — Windows fails closed
    at startup. Five new flags / env vars: --saml-idp-entity-id (YUZU_SAML_IDP_ENTITY_ID),
    --saml-idp-sso-url (YUZU_SAML_IDP_SSO_URL), --saml-idp-cert (YUZU_SAML_IDP_CERT),
    --saml-sp-entity-id (YUZU_SAML_SP_ENTITY_ID), --saml-sp-acs-url (YUZU_SAML_SP_ACS_URL).
    Audit: auth.saml_login (result=ok) / auth.saml_login_failed (result=error). Partially closes
    /auth-and-authz gap-matrix P1 #6. Fast-follow work (observability, proxy-TLS/HA, IdP metadata,
    group→role mapping, AuthnRequest signing, hardening) is tracked in #1789. See
    docs/auth-architecture.md "SAML 2.0 SP", docs/user-manual/authentication.md "SAML 2.0 SSO",
    and the security review docs/security-reviews/saml-sp-2026-07-01.md.

  • /inventory Devices tab shows real device-CI data. The Serial/Model/CPU-RAM
    columns (previously greyed placeholders) now read from DeviceInventoryStore, and
    the per-device drill grows a full CI-record panel (manufacturer, model, serial,
    system UUID, domain/OU, BIOS, CPU, memory, MAC addresses, NIC count, OS
    name/version/build, architecture, first/last-synced) above the installed-software
    list. Disk capacity and owner/location are deliberately withheld (a macOS
    disk-collection fix and operator-set CMDB fields, respectively, are unbuilt
    follow-ups). List enrichment never attaches an out-of-scope device's CI — the join
    only looks up agent IDs already present in the roster the route renders
    (regression-tested); per-operator management-group confinement of that roster itself
    is designed for, not yet verified effective (ADR-0017). New audit verb
    inventory.device.ci (per-device drill) and an extended inventory.devices (fleet
    list) both use the behavioural-PII audit tier, since serial/system UUID/MAC are
    device-persistent identifiers (GDPR personal data per ADR-0016); the fleet-list verb
    now also audits result=failure (not just success) when the CI-enrichment join
    itself degrades, so the audit trail can't misreport a partial read as clean.

  • Device-identity daily-sync source + CMDB store (device_ci, ADR-0016 source #3).
    The agent now syncs stable hardware/OS identity to central Postgres daily, on the same
    hash-skip framework as installed_software. The device_ci source fans out to the
    hardware, device_identity, os_info, and network_config plugins in-process
    (LocalDispatcher) and emits a canonical CI record: manufacturer, model, serial number,
    system UUID, BIOS details, CPU/RAM/disk summary, primary MAC, NIC count, OS
    name/version/build, and architecture. Volatile telemetry (free disk space, uptime, IP
    addresses) is deliberately excluded so the content hash is stable and steady-state
    traffic is suppressed. A new hardware plugin system action collects serial + system
    UUID (Windows WMI / Linux sysfs-DMI via cap_dac_read_search / macOS ioreg); it is
    also dispatchable on-demand via the audited instruction path. Server:
    DeviceInventoryStore (born-on-PG, schema device_inventory_store, one row per agent;
    ingest via device_ci_ingestion.* on both the direct ReportInventory and gateway
    ProxyInventory paths). last_seen/first_seen are the server receipt time (#1685).
    Suppressed by --inventory-disable along with the other sources. Observability:
    yuzu_inventory_ingest_total{source="device_ci"},
    yuzu_inventory_ingest_duration_seconds{source="device_ci"},
    yuzu_inventory_read_degrade_total{source="device_ci"}; the store joins /readyz +
    /healthz. The operator-facing read surface is the /inventory Devices tab bullet
    above.

  • /auto VERIFY — before/after application-performance evidence (UAT non-functional). A third
    stage on the /auto page (after ASSESS pre-flight and ACT deploy): did upgrading an app from one
    version to the next change how the same machines perform? The shift is computed per machine,
    paired
    — each device's own baseline-version window vs its own candidate-version window (read from
    the shipped per-device B1 store, the window anchored to that machine's transition, not to
    "today" — so a staggered rollout still pairs), then the per-machine deltas are aggregated. A fleet
    baseline-vs-candidate diff would be confounded by different populations; pairing on the machine
    holds it fixed. A machine that ran only one version in-window is excluded and counted, never
    imputed. Evidential only — there is no verdict, no threshold, no pass/fail: the tool reports the
    measured shift (CPU/working-set before→after means, the median per-machine delta, p95 across
    machines) and the up/flat/down split; the operator (or an AI colleague over MCP) judges. There is
    no cohort floor — real canaries are 2–3 devices, so a floor would gut the feature; a sub-floor
    paired set is flagged indicative, never suppressed, and the read is audited
    (dex.app_perf.compare, operational) — accountability standing in for the suppression a floor
    would give. Surfaces (all GuaranteedState:Read): REST GET /api/v1/dex/perf/compare, MCP
    compare_app_perf_versions, and the /auto dashboard VERIFY stage (aggregate cards + distribution

    • an audited per-machine drill). Pure engine app_perf_compare (reducer + compare split so a
      later live candidate plugs the same slot); B1 cohort read app_perf_cohort_reader (agent_id
      preserved). The per-machine pairs are a dashboard-only audited drill
      (dex.app_perf.compare.drill); REST/MCP expose only the identity-free aggregate.
      Deferred: a REST audited-fail-closed per-machine drill, per-version crashes/hangs (the
      central crash-store join), live measure-right-after-deploy (fan-out procperf), and the
      deploy→verify cohort auto-fill.
  • TAR retention-paused source purge (Phase 15.A). The /tar retention-paused frame gains a
    Purge data action: for a source an operator deliberately paused (to preserve forensic data),
    permanently drop its accumulated warehouse rows (<source>_{live,hourly,daily,monthly}) without
    re-enabling collection. New agent action tar.purge_source (refuses unless the source is paused —
    the authoritative TOCTOU guard, held atomically with the delete under the collector lock), dashboard
    fragment POST /fragments/tar/retention-paused/purge and an agentic-first REST endpoint
    POST /api/v1/tar/retention-paused/purge — both gated on Infrastructure:Delete (per-device scoped),
    a typed-hostname confirmation (native prompt(), CSP-safe) + CSRF same-site gate on the fragment,
    audit verb tar.source.purge, and metric yuzu_tar_source_purge_total{result}. The generic
    POST /api/command path also elevates tar.purge_source to Infrastructure:Delete + visible-agent
    confinement so it can't be a weaker route. The purge also forces a wal_checkpoint(TRUNCATE) so the
    erasure is complete on disk (no WAL residue) and disk is reclaimed at return. Completes 15.A. See
    docs/tar-dashboard.md §3.4.
    Upgrade note: the tar.purge_source action requires an agent at this release or later; an older
    agent returns error|unknown action: purge_source (it does not crash), but because dispatch is
    fire-and-forget the dashboard still shows "Purge dispatched" — verify the outcome with a fresh Scan.

  • JIT (just-in-time) admin elevation (POST /api/v1/elevate, SOC 2 CC6.3/CC6.6). Reduces standing
    privilege: a pre-authorized operator (the new per-user users.elevation_eligible flag, auth.db
    migration v5, admin-managed via POST /api/v1/users/<name>/elevation-eligibility) holds a non-admin
    base role day-to-day and activates admin just-in-time for a bounded, justified window, then
    auto-reverts. The session's effective_role() is admin only while elevated; require_admin and the
    permission gates honour it. Elevating requires the operator to be MFA-enrolled (mandatory regardless
    of --mfa-enforcement — elevation is the privilege boundary) and pass a fresh MFA step-up. Window
    capped by --jit-max-elevation-secs (YUZU_JIT_MAX_ELEVATION_SECS, default 3600, max 86400).
    POST /api/v1/elevate/revoke for manual step-down. The grant audit is fail-closed (an unrecordable grant
    is rolled back, not granted); revoking eligibility immediately ends active elevations; self-grant of
    eligibility is blocked. Audit: role.elevation.{granted,denied,revoked} + user.elevation_eligibility.set.
    Elevation is in-memory per cookie session (a restart/logout drops it) and cookie-sessions-only — API
    and MCP tokens can never be elevated. Closes /auth-and-authz gap-matrix P1 #9. See
    docs/auth-architecture.md "JIT admin elevation".

  • Idle (inactivity) session timeout (--session-inactivity-secs, SOC 2 CC6.3). Operator dashboard
    cookie sessions can now be invalidated after a configurable period of inactivity — a sliding
    window that resets on each authenticated request, under the existing absolute 8h session lifetime.
    Wires the previously-reserved sessions.last_activity_at column end-to-end (enforced in
    AuthManager::validate_session; best-effort throttled auth.db mirror via
    AuthDB::touch_session_activity). Default 0 = disabled (opt-in; existing deployments unaffected;
    recommended 900 = 15 min). Scope is cookie sessions only — API tokens and MCP tokens are never
    idle-timed-out
    , and OIDC users re-authenticate via SSO. YUZU_SESSION_INACTIVITY_SECS. Closes
    /auth-and-authz gap-matrix P1 #8. See docs/auth-architecture.md "Inactivity session timeout".

  • Hardened authentication mode (--auth-mode=sso-only) + break-glass account (SOC 2 CC6.3/CC6.6).
    Local-password login can be disabled fleet-wide so only OIDC SSO mints a session
    (--auth-mode=sso-only / YUZU_AUTH_MODE). A rejected local login returns the same generic 401 as a
    bad password (no enumeration/mode oracle) and is counted via yuzu_auth_local_disabled_total (metric,
    not a per-attempt audit row). A single --break-glass-user is exempt only while armed, armed
    out-of-band via the host CLI yuzu-server --break-glass-arm (auto-expiring, default 24h via
    --break-glass-window-secs; audited auth.breakglass.armed, attributed to the kernel OS identity).
    Mandatory MFA is enforced fail-closed at boot and at login (an un-enrolled break-glass login is
    hard-denied 403, never offered enrollment). Use is audited at Severity::kCritical
    (auth.breakglass.login / auth.breakglass.denied) + metric yuzu_auth_break_glass_login_total.
    AuthDB migration v4 adds a nullable users.break_glass_armed_until column (additive, data-safe).
    Opt-in: existing deployments default to standard and are unaffected. Enabling sso-only
    refuses to start without --oidc-issuer configured and (if set) a --break-glass-user that
    exists and has MFA enrolled — configure OIDC and pre-enroll the break-glass account before
    restarting. See docs/auth-architecture.md "Hardened mode", docs/user-manual/upgrading.md, and the
    docs/ops-runbooks/auth-db-recovery.md break-glass arm runbook. Closes /auth-and-authz P0 #3.

  • /auto deploy — stage + execute an upgrade on a pre-flight go-cohort. The /auto page
    gains an ACT stage after the ASSESS (pre-flight) stage: as soon as a pre-flight run has a
    go-cohort (≥1 device in bucket go / warn-only — the run need not be complete; the button
    appears with the first cleared device and the count grows mid-run), Deploy go-cohort
    stages an installer (download + SHA-256 verify) and then executes it on the devices cleared
    at click time, tracking a per-device stage→execute state machine. A re-deploy of the same run
    excludes devices an earlier deployment already installed (cross-deployment execute-once).
    Pre-flight completion is now event-driven (the result self-poll completes a run the moment its
    cohort settles, not on the up-to-60s background-runner tick). Built on the existing content_dist plugin (stage /
    execute_staged); no new agent code. Born-on-Postgres DeploymentRunStore (schema
    deployment_run_store) persists the artifact spec, the frozen cohort, and each device's
    step. The result is aggregate-first (a KPI strip + progress bar headline the counts;
    the per-device list is problem-first and render-capped) so it reads at fleet scale. Driven
    by an operator today; the engine (deployment::advance) is HTTP-agnostic so an agentic
    worker can drive the same path via MCP later. Safety: the execute step MUTATES, so —
    unlike the read-only pre-flight checks — it is dispatched at most once per device
    (claim_for_exec commits staged→executing before the command leaves the server; this
    run-once guarantee survives concurrent advances and a server restart), and execute-once is
    also enforced across deployments by a create-time resume guard (a partial unique index +
    find_running_for_run) so re-clicking Deploy re-attaches to the in-flight run instead of
    re-installing. Every advance re-authorizes against the operator's current visible set
    (devices_fn(viewer) ∩ cohort); a device the operator has lost scope to is skipped, never run.
    Slice-1 liveness is page-driven (no background runner): a closed/timed-out page pauses the
    deployment durably and is resumed by re-opening it. A human confirm() gates the deploy. RBAC:
    SoftwareDeployment:Read opens the config; the result poll needs Read+Execute (it advances
    the engine); create needs Infrastructure:Read+Execute; delete needs Execute; owner-scoped;
    operational deployment.{create,advance,delete} audit. URL-only on /auto (not a new nav tab).

  • Inventory dashboard (/inventory). A point-and-click view over the daily-sync
    installed-software data (ADR-0016). Three tabs: Software (the fleet software list —
    title, publisher, install count, distinct versions, with an installs-per-version drill),
    Devices (a thin, offline-survivable device list sourced from the persisted endpoint
    state + the live registry; click a device for its installed software), and Find software
    (which devices run a given title). The catalogue/version counts are served from a
    precomputed rollup (catalog_rollup / version_rollup / catalog_rollup_meta,
    refreshed hourly by a background SoftwareCatalogRollup thread, keep-last-good on
    failure) so page reads never run a full-table GROUP BY — the underlying data changes
    only on the daily sync. The KPI strip shows an "updated N ago" stamp and a "building"
    state before the first refresh. Gated on the existing Inventory:Read (per-device
    reads use the management-group-scoped gate); fleet-wide catalogue/find counts are not yet
    management-group scoped (ADR-0017 confinement inert under the global gate — caveated in the
    UI). On store degradation the authoritative reads (Software/Find/per-device-software) show an
    "unavailable" banner, never an empty table (ADR-0016 §7); the fail-soft device roster may show an
    empty list during a database incident (its empty-state copy says so). New audit verbs
    inventory.software.{catalog,versions}, inventory.device.software, and inventory.devices (the
    Devices roster read — set-and-proceed); inventory.software.query (pre-existing on the REST
    endpoint) is now also emitted by the dashboard Find tab and documented.
    New nav item + command-palette entry.

  • DEX app-performance over time — per-device drill dashboard UI. The per-device app-perf
    history (REST GET /api/v1/dex/devices/{id}/app-perf, shipped in slice 2) now has a dashboard
    surface: an "Application performance over time" panel on the /device DEX drill, beside the
    live Top-applications query. It reads the central B1 store (no live device query, no
    Execute permission — a read-only operator can open it), groups each application with its
    versions as sub-rows, and renders a per-(app, version) CPU-over-time sparkline plus the
    window's sample-weighted avg / peak CPU and working set, so a per-version regression on one
    device reads straight off adjacent rows. Behavioural PII: per-device-scoped
    GuaranteedState:Read + the same dex.device.app_perf.view audit verb as the REST drill,
    under the dashboard set-and-proceed posture (sets Sec-Audit-Failed but still renders, distinct
    from the REST fail-closed surface). A new pure reduction app_perf_device_summaries (in the
    shared dex_app_perf_model) feeds the renderer; REST stays raw daily rows; still no MCP twin
    (the fail-closed contract can't be expressed on MCP). Deferred: the per-version crashes/hangs
    join (a separate central crash-store join on (name, version)).

  • DEX app-performance over time (slice 2) — the operator read surface. The B1/B2 stores
    below are now readable. REST (all GuaranteedState:Read): GET /api/v1/dex/perf/apps (the
    picker — apps with retained data), GET /api/v1/dex/perf/app?app=&version= (fleet trend, per
    (version, day)), GET /api/v1/dex/perf/group?group_id=&app=&version= (one management group's
    on-the-fly trend), and the per-device GET /api/v1/dex/devices/{id}/app-perf. MCP twins
    (read-only): list_dex_perf_apps, get_dex_app_perf, get_dex_group_app_perf (the per-device
    drill has no MCP twin; its dashboard panel is in the entry above). Dashboard: a picker + per-version trend on the DEX
    Performance tab, fleet-wide or scoped to a group via a selector. One shared pure transform
    (app_perf_fleet_trend/app_perf_group_trend/app_perf_version_summaries) feeds REST, MCP and
    the dashboard so they cannot disagree. Privacy: both the fleet AND group aggregates suppress
    any (version, day) point below kDexCohortFloor (10) devices to a count only — a sub-floor
    aggregate singles out an individual even without an agent_id; percentiles are
    bucket-resolution (lower_bound/"≥" when in the open top bucket) and withheld (hist_stale)
    for a row under a superseded histogram scheme. The per-device drill is behavioural PII — scoped
    to the caller's management group and audited fail-closed (dex.device.app_perf.view; 503 +
    Sec-Audit-Failed if the audit row can't persist). Aggregate reads are not individually audited
    (cohort posture). Group trend reads B1 (≤31 days); fleet trend reads B2 (≤180 days); per-app
    sampling stays opt-in (procperf_enabled, off by default), so data appears only after the first
    completed UTC midnight on an opted-in device. Deferred: per-version crashes/hangs join and a
    device-drill MCP tool. (The per-device drill's dashboard UI shipped — see the entry above.)

  • DEX app-performance over time (B1) — per-device daily app-version perf, centralized.
    New daily-sync source app_perf (agent) rolls the on-device procperf_hourly warehouse up to
    per-(app, version, day) daily summaries — sample-weighted CPU/working-set, max-of-max peaks,
    the last 2 completed UTC days per cycle — and ships them over the existing ReportInventory
    transport as a new plugin_data key (no proto change, no gateway regen). New born-on-Postgres
    AppPerfDailyStore (schema app_perf_daily_store, 31-day retention, plain table + per-agent
    prune) persists them via a shared ingest seam wired identically on the direct ReportInventory
    and gateway ProxyInventory paths, so fleet questions like "which devices ran v124 and how did
    it perform" become answerable without federating to every endpoint. Version is canonicalized at
    ingest (the same canon_version the stability side uses) so perf joins app stability by
    (app, version). Hash-less (perf changes daily → always full); scope is resource-significant
    (procperf top-N) app-versions, not a full app census. Windows-fed today (procperf is
    Windows-only); gated by procperf_enabled + the --inventory-disable daily-sync master switch.
    Its read surface (per-device drill + group trend) shipped in slice 2 — see the entry above.

  • DEX app-performance over time (B2) — fleet-aggregate trend substrate. New born-on-Postgres
    AppPerfFleetStore (schema app_perf_fleet_store, 180-day retention) holds one row per
    (app, version, UTC day): fleet device-count, exact CPU/working-set sums + maxima, and a
    fixed-bucket histogram of per-device daily values (CPU low-end-weighted, working-set log-scale)
    so true fleet percentiles (p50/p95) are computable over the long window without storing
    per-device rows. Built from B1 by AppPerfRollup — the ADR-0012 cross-store query owner — as one
    server-side INSERT … SELECT … ON CONFLICT per day (histogram via COUNT(*) FILTER over
    half-open buckets; no per-device data crosses the wire), driven by an hourly background thread
    that re-rolls a 4-day trailing window (idempotent; absorbs late-arriving B1) and prunes beyond
    180 days. Full version grain (coarsen to major.minor on read). Histogram boundaries are frozen
    (hist_version stamps the scheme); derived percentiles are bucket-resolution approximations.
    Its read surface (fleet trend + picker, REST/MCP/dashboard) shipped in slice 2 — see the entry
    above. The aggregate carries no agent_id, and sub-floor (version, day) points are suppressed
    to a count only on read — no per-device attribution, no singling-out.

  • Pre-flight readiness page (/auto). New dashboard page for operator-initiated go/no-go checks
    across a scoped device cohort before a fleet change. Checks: application version (min/max), OS version
    floor, OS architecture, minimum free disk, and pending-reboot status — grouped per device
    (Pass / Failed / Warn-only / Incomplete) with a run-level summary. Runs persist in a new born-on-Postgres
    PreflightRunStore (schema preflight_run_store) for 14 days and are owner-scoped (an operator sees only
    their own runs; the result read is owner-scoped at the store seam, so another operator's run is
    indistinguishable from not-found). A background runner re-dispatches the read-only checks to devices that
    reconnect within the run window. Audit verbs: preflight.run (gates Infrastructure:Read + Execution:Execute)
    and preflight.run.delete (gates Execution:Execute). Reached by URL — not a nav tab.

  • disk_space agent plugin — cross-platform free-space probe. New plugin (Windows/Linux/macOS),
    single free action: returns total bytes, free bytes, and percent used for a volume (path param;
    default C:\ / /). Free is quota-aware caller headroom (FreeBytesAvailableToCaller on Windows,
    f_bavail on POSIX). Content definition crossplatform.storage.free.

  • Device page — live Disk space card. New card on the device "Get live info" lens dispatches
    disk_space/free on demand; per-volume table with a colour-coded usage bar (>=90% used or <5 GiB
    free = red, matching the storage.low DEX threshold). Audit verb device.live.disk.

  • REST endpoint for fleet-wide installed-software inventory (ADR-0016 follow-on).
    GET /api/v1/inventory/software exposes the typed SoftwareInventoryStore over REST
    (gated on Inventory:Read), the agentic-first sibling of the query_installed_software
    MCP tool. Filter by name / agent_id (omit both for a fleet scan); limit capped at
    1000 with result_truncated_by_cap when more rows exist. A per-agent management-group
    drop filter is applied — out-of-scope devices are dropped and counted in devices_omitted
    (a positive value means matching software exists outside your scope, not "absent
    fleet-wide") — but this confinement is not yet verified effective under the global
    Inventory:Read gate (ADR-0017; see docs/user-manual/inventory.md §Scope),
    with a distinct scope-denied audit row. On store degradation it returns 503 (an A4
    error envelope), never an empty 200, so a vulnerability query cannot read a
    transient Postgres outage as "installed nowhere" (ADR-0016 §7 authoritative reads). A
    software dashboard / per-device drill-down remain planned follow-ons.

  • Agent daily-sync framework + installed-software inventory in Postgres (ADR-0016). The agent now
    pushes endpoint state to the server on a per-source daily cadence over ReportInventory, starting
    with installed software (machine-wide scope; no per-user/PII). It is kind to the network at
    fleet scale: each endpoint spreads its sync by a stable per-agent phase offset (no lockstep), and
    when a source's content is unchanged since the last successful sync it sends only a content hash
    instead of the full list (hash-skip); the server replies need_full to force a resend on a cold
    cache, with a weekly full-floor as a backstop. Installed software lands in a new born-on-Postgres
    SoftwareInventoryStore (normalized rows — portable SQL, no JSONB — so fleet-wide queries
    like "which devices run X" are first-class), via a shared ingest seam wired identically on the
    direct and gateway paths. Reads are gated on a new Inventory RBAC securable (Inventory:Read).
    Reuses the existing installed_apps plugin (Windows/Linux/macOS) in-process — no new collector.
    Hardened for fleet-scale resilience: a per-source blob cap sized below the gRPC message ceiling,
    exponential agent-side backoff on consecutive need_full resends (so a server cold-cache or store
    outage cannot drive a flat-cadence full-resend storm), a yuzu_inventory_ingest_total{source,outcome}
    metric, and the store wired into both /readyz and /healthz. Readable now via the
    query_installed_software MCP tool (Inventory:Read, filter by name/agent, management-group
    scoped so an operator sees only their own devices) — distinct from the generic query_inventory
    tools, and via GET /api/v1/inventory/software (REST; see the separate entry above). A
    software dashboard is a planned follow-on. A deploy-time opt-out
    (--inventory-disable / YUZU_AGENT_INVENTORY_DISABLE) disables collection entirely for
    privacy-sensitive / works-council jurisdictions. Inventory fields are sanitized to valid UTF-8
    (invalid bytes → U+FFFD) and truncated on codepoint boundaries — byte-coordinated between agent and
    server — so a non-UTF-8 registry string can never trigger a PostgreSQL TEXT-reject resend loop;
    concurrent full-replaces for one agent are serialized with a transaction-scoped advisory lock (no
    row/hash divergence); a transient empty collection is skipped rather than wiping stored inventory;
    an over-cap blob raises a dedicated dropped-outcome alert (it never self-heals); and
    query_installed_software reports devices_omitted so a scoped caller can distinguish "outside my
    scope" from "not installed". Reads are authoritative (ADR-0016 §7): query_installed_software
    returns a JSON-RPC error — never a silent success with empty rows — when the Postgres store is
    degraded (pool/query failure), so a fleet vulnerability query can never read a transient backend
    hiccup as "installed nowhere". An ingest report carrying an implausibly large source map is rejected
    wholesale, and concurrent-replace serialization uses a 64-bit advisory-lock key.

  • TAR — software install/uninstall capture source ($Software). A new TAR
    warehouse source records application installed / removed / upgraded events over
    time by diffing the installed-software inventory on a dedicated hourly trigger
    (tar.software; tune the cadence via software_interval, 0–86400 s). On Windows it
    captures machine-scope only (HKLM Uninstall 64-bit + WOW6432Node) installs — no
    per-user enumeration, no Windows profile names, and no user identity / no PII.
    Tiers: $Software_Live (5000 rows) →
    $Software_Daily (31 d) → $Software_Monthly (12 mo), with per-name
    install/remove/upgrade counts. The source is off by default (opt-in) — an operator
    enables it per host with tar.configure software_enabled=true. Even though the
    machine-scope data carries no user identity (device asset-management /
    vulnerability-relevance data, like Services and User sessions), a brand-new capture
    source ships disabled out of caution so a host only collects it once the operator has
    decided to; enabling or disabling re-baselines the source so neither emits a spurious
    install/remove storm. Names, versions, and publisher only
    — no command lines or usage data. The first
    scan on a host seeds the baseline silently so an installed event always means
    "installed now". tar.status reports a software_last_run_ts heartbeat. On upgrade,
    tar.status gains a software_* block plus the software_interval_seconds /
    software_last_run_ts lines
    — update any field-count parsing.
    Linux (dpkg/rpm) and macOS (pkgutil) collectors are a fast-follow — the
    $Software_* tables are queryable but empty there until then. Disabling the source
    (software_enabled=false) is atomic with the collector — it holds the same
    software_collect_mu_ the collector takes and the collector re-checks the flag under
    that lock, so a disable racing an in-flight scan can never insert events from the paused
    window or leave a stale baseline (no ghost install/remove/upgrade events on re-enable;
    same #538 contract as the other snapshot-diff sources). SystemComponent entries are
    read as REG_DWORD (their canonical type) so system components/patches are correctly
    excluded, and software rows are reachable from the legacy tar.query/tar.export
    typed paths as well as tar.sql. Enumeration is bounded by a per-cycle entry cap
    (kSoftwareEntryCap, warns once on truncation) so a bloated/corrupt registry cannot grow
    the tick unbounded, and same-name dedup compares versions numerically (so 10.0
    outranks 9.0 — a lexicographic compare previously suppressed real upgrades). tar.snapshot
    now collects the source too. See docs/user-manual/tar.md and the data-handling
    classification in docs/enterprise-readiness-soc2-first-customer.md.

  • DEX: wave-4 reliability signals — power, driver, and service health (Windows).
    Seven new signal types (Windows event catalogue 103→110, server display catalogue
    107→114), all real-record-pinned from a live HP ZBook Firefly 14 G8:
    os.modern_standby_exit (Kernel-Power 507 — deep-idle/DRIPS residency %; fires on
    every resume, scores as benign by default), network.adapter_driver_dump
    (Netwtw10 7025 — Intel Wi-Fi D3 dump; vendor-coupled to Intel Wi-Fi),
    hw.driver_load_failed (Kernel-PnP 219 — driver failed to load; distinct from
    device-start-fail 411), hw.battery_error (Kernel-Power 521 — faulted/abandoned
    batteries only; benign battery-count changes self-suppress), service.unresponsive
    (SCM 7011 — runtime control-timeout; params reversed vs 7022, so it is its own
    type), service.shutdown_failed (SCM 7043 — unclean service shutdown), and
    network.adapter_reset (NDIS 10317 — vendor-neutral NIC fatal/reset, the
    generic complement to the Intel-specific dump). os.power_loss (Kernel-Power 41)
    is enhanced (not new): PowerButtonTimestamp now discriminates a held power
    button (hard-hang recovery / manual power-off) from a sudden supply loss. Adds a
    SignalObservation::suppress primitive (an extractor can veto a matched benign
    event). Full signal table in docs/dex-signal-catalog.md Wave 4.

  • DEX: per-event observation detail panel. Clicking any row in a device's DEX
    signal history loads a detail panel showing every captured projection field for
    that one event (subject, reason, symbolic name, component, metric, platform,
    exact timestamp, event ID). Fields already in the store — no agent or wire change.
    Per-device-scoped (GuaranteedState:Read), bound to the event's own device (a
    foreign event ID returns an opaque 200 placeholder, indistinguishable from a
    missing event), and audit-logged (dex.observation.view,
    recording the obs_type for works-council countability).

  • DEX: Applications lens (Apps tab). A new top-level DEX tab ranks fleet
    applications by reliability signals (crashes and hangs, keyed on the process
    image), each row drilling to the existing per-app blast-radius view. Built on the
    existing dex_top_apps aggregation — no new agent collection. The DEX sub-nav is
    now Overview · Apps · Catalogue · Health score · Trends · Performance · Network.

  • Guardian — name-anchored, device-applicable compliance REST.
    New GET /api/v1/guaranteed-state/device-compliance?baseline={name}&agent_id={id}
    looks a Baseline up by name (a stable constant such as ServiceNow Compliance, not
    a churning baseline_id) and returns the Guards actually applicable to the device
    each with the device's last reported verdict (compliant | drifted | errored |
    pending), plus counts and a last_updated freshness stamp. One Baseline carries a
    superset of Guards, each scoped via scope_expr, so the push arms a different
    subset per machine; the endpoint returns the deployed_snapshot intersected with the
    Guards the device has reported, so an out-of-scope Guard is absent (not pending)
    and two machines querying the same Baseline name legitimately see different Guards.
    total_guards is that applicable count. A not-yet-deployed Baseline returns
    deployed:false with empty guards (consumer renders "No Baseline Deployed"), and
    member edits appear only after a re-deploy. A deployed Baseline returning
    total_guards:0 with last_updated:null is not assessable (the device is
    offline / newly-enrolled, or every Guard is out of its scope), not compliant — a
    CMDB consumer must cross-reference device liveness and never render 0/0 as green.
    Designed for embedding Guardian compliance into an external CMDB / ITSM CI record
    (e.g. ServiceNow).
    Authorization is per-device-scoped GuaranteedState:Read (a global grant passes
    fleet-wide; a management-group-scoped principal must hold Read via a group the
    device is in — a previously group-scoped token now gets 403 for out-of-scope
    devices, where a flat global check would have passed them; global tokens are
    unaffected). Audited guardian.device.view on access (denials audited at the auth
    layer as auth.scoped_permission_required). A behavioural-PII read, so it fails
    closed
    : if the guardian.device.view audit row cannot persist it returns 503 +
    Sec-Audit-Failed: true and withholds the compliance body (parity with
    GET /api/v1/dex/devices/{id}, governance #1549) — the 503 is returned before the
    404, so an audit outage never reveals baseline existence without durable evidence
    (CC7.2). Behaviour change for API consumers: an audit-store outage now yields
    503 where this unreleased route previously returned 200; on the fleet-polled
    CMDB path a sustained outage 503s every poll fleet-wide (no degraded-serve
    fallback). Both query params are required, length-capped (256 /
    auth::kMaxAgentIdLength), and rejected if they contain control characters
    (bytes < 0x20) → 400; the 400/404/503 bodies use the A4 envelope
    (correlation_id), while the 403 is the auth/RBAC layer's denial body (not the A4
    envelope; exact shape varies by denial reason — RBAC vs service-scope).
    The response carries a machine-readable assessable flag (false for a draft
    Baseline or a deployed Baseline this device has reported no applicable Guard against —
    the consumer must not compute a 0/0 compliance %) and snapshot_total (the
    Baseline's full deployed-member superset, an upper bound on coverage). Caution: the
    report-driven denominator can over-estimate compliance on a partial report (an
    in-scope-but-unreported Guard is absent, not pending), so consumers must not gate on
    compliant/total_guards until the per-device scope_expr computed denominator lands
    (deferred). X-Correlation-Id is set on every response (parity with the
    dex.device.view siblings); a guard reported with an unrecognized verdict token keeps
    its real updated_at (no longer suppressed); a baseline-store fault returns a
    retryable 503 (not the 404 a CMDB would read as "delete this CI") on a transient
    fault; and the guard-name lookup chunks its IN-list so a Baseline larger than
    SQLITE_MAX_VARIABLE_NUMBER still resolves every name. A YuzuAuditPersistFailures
    Prometheus alert (docs/prometheus/yuzu-alerts.yml) now fires when behavioural-data
    routes 503 fleet-wide on an audit-store outage.

  • TAR-styled live device snapshot ("Get live info", expanded). The per-device
    page's Get live info button now returns a full live system snapshot — a KPI strip
    (uptime, process / service / connection / user counts) over a grid of collapsible,
    uniformly-sized, scrollable cards with an Expand all / Collapse all control and a
    per-card pop-out for a larger view. Each card is a separate live read-only dispatch
    through the existing Execute-gated, audit-logged chokepoint: Processes (a parent→
    child tree with the SHA-256 of each on-disk image and live network connections joined
    by PID, suspicious parent→child spawns flagged), Services, Adapters & IP,
    ARP (Windows), DNS cache (Windows), Listening ports, Active connections,
    Logged-in users, and a read-only Capture sources view (which TAR warehouse
    sources are capturing locally; configuration stays on the TAR page). Two new agent
    actions: processes/list_tree (adds parent PID for tree reconstruction) and
    network_config/arp (Windows GetIpNetTable2) — these ship with agents built from
    this release onward; an older agent renders an empty Processes-tree / ARP card (it
    returns an unknown action response, so upgrade the agent to populate those cards). Each card has its own
    device.live.<kind> audit verb so usage-class reads (process tree, connections, users,
    DNS cache) stay separately countable for works-council. ARP and DNS-cache are
    Windows-only; on other platforms those cards render an honest "not available on this OS"
    note. Requires Execution:Execute + GuaranteedState:Read, scoped to the device. The
    previous flat "Running processes" panel (kind=processes) is retained for
    REST/scripted callers; the dashboard now dispatches kind=process_tree. REST/scripted
    callers reach kind=uptime|processes only — the 9-card grid is dashboard-only pending
    the A1 JSON backfill (#1649). See docs/user-manual/device-management.md.

  • ARP + DNS capture sources (TAR, ADR-0015) — Windows. Two new opt-in
    (default_enabled=false) TAR capture sources: arp (host ARP / neighbour table
    via GetIpNetTable2 → $ARP_Live/$ARP_Hourly) and dns (device DNS
    resolver-cache state via DnsGetCacheDataTable → $DNS_Live/$DNS_Hourly —
    device-level, no per-process attribution; DNS is usage-class PII, enabling is
    audited). Queryable via tar.sql, the query/export actions (type: arp|dns),
    and the crossplatform.tar.recent_arp/recent_dns canned instructions; toggled
    via crossplatform.tar.configure (arp_enabled/dns_enabled) or the new /tar
    "Capture sources" frame
    (staged-then-push guardrail + category filter). The
    process-tree pane gains device-level DNS-cache + ARP-table panels
    (audit verbs tar.dns.read/tar.arp.read). Linux/macOS collectors are planned
    (schema registered, queryable-empty) and recorded per-OS in
    docs/os-capability-matrix.md. The Windows agent links dnsapi; the resolver-cache
    read combines DNS_QUERY_NO_WIRE_QUERY with the additional cache-read flag required
    to surface cached records on current Windows 10/11 builds.

  • MCP query_responses closes the dispatch→collect loop by execution_id. The tool now
    accepts an execution_id argument (routed to ResponseStore::query_by_execution) so an
    agentic worker that dispatched via execute_instruction can collect exactly that run's
    responses — exact-correlation, no cross-execution bleed — instead of the definition-wide
    instruction_id collect. At least one of execution_id/instruction_id is now required (was
    instruction_id-only); when both are given, execution_id wins. Each returned row now echoes
    its execution_id. The advertised status filter is corrected to integer in the tool schema
    (the handler always read it as the CommandResponse status enum; the prior string declaration
    was wrong), and limit is now clamped to [1,1000] (a negative limit previously bound as an
    unbounded SQLite LIMIT -1, and limit:0 returned zero rows a worker could misread as "done").
    Foundation for fleet-scale agentic fan-out across tens of thousands of devices. (Scope: limit
    caps a page at 1000 rows; correct collection of executions that fan out to more than 1000 devices
    is a keyset-pagination follow-up — offset paging is deliberately not exposed, as it skips/dupes
    rows while responses are still arriving. The sibling aggregate_responses tool still keys on
    instruction_id only; an execution_id aggregate is a follow-up. MCP-first: the streaming REST
    surface GET /api/v1/events?execution_id= already exists; a non-streaming polling REST collect
    by execution_id is a deferred follow-up slice.)

  • Sampled authentication-log evidence export (SOC 2 CC7.2). New
    GET /api/v1/audit/auth-sample?from=&to=&limit= returns a pseudo-random
    sample of authentication-surface audit events (action prefixes auth.,
    mfa., session.) over an optional epoch-seconds window — a representative
    sample for auditor evidence rather than the latest-N. Gated on AuditLog:Read
    (not admin-only, so a read-only auditor role can pull evidence without full
    admin — separation of duties), and the export is itself audited as
    audit.auth_sample.exported. Backed by two new AuditQuery knobs
    (action_prefixes OR-group filter + random_sample ordering); discoverable
    in the OpenAPI spec (A2). Closes the P0 auth gap "sampled auth-log evidence
    export".

  • Guardian ingest-drop counter. New /metrics counter
    yuzu_server_guardian_events_dropped_total counts cumulative Guardian events dropped at
    ingest on an event_id PK/UNIQUE conflict (redelivery, an agent event_seq_ reset, clock
    skew, or a forged-id pre-claim). > 0 distinguishes "no drift observed" from "drift observed
    but silently discarded" (CC7.3 evidence). Sits alongside
    yuzu_server_guardian_events_{total,written_total,reaped_total}. (#1414)

  • Offline hosts stay visible on the fleet map. A new born-on-Postgres store
    (endpoint_state) records each agent's last-known identity + last-seen on every heartbeat, so
    a host that drops out of the in-memory 60 s topology cache renders stale-flagged on
    /viz/fleet instead of vanishing. New pool observability on /metrics
    (yuzu_pg_pool_{in_use,open,size}, yuzu_pg_connect_failed_total,
    yuzu_pg_acquire_timeout_total, yuzu_pg_unhealthy_discard_total,
    yuzu_pg_acquire_wait_seconds) and a live pool probe + the new store join the /readyz
    readiness conjunction.

  • Shared device pages. New dashboard pages /devices (searchable fleet list with an OS
    filter, online status, and per-device DEX score) and /device?id= (the per-device entity
    page, reachable from any dashboard, with Device info / DEX / Guardian lens tabs). The
    fleet list now requires Infrastructure:Read (parity with /api/agents, via the same
    get_visible_agents_json provider — closing the prior any-authenticated-operator access);
    every per-device route — page, info, the DEX/Guardian lenses, and the live pull — is
    additionally scoped to the device's management group via require_scoped_permission, so an
    operator cannot open, read, or live-query a device outside their scope. The DEX and Guardian
    lenses render per-device behavioral/compliance data
    with audit-on-open (dex.device.view, guardian.device.view). The fleet list and a single
    page open score only the rows they render — opening one device no longer scores the whole
    fleet. The list shows currently-connected devices only (no offline/status filter yet). The
    DEX dashboard's per-device drills the device DEX lens links to (/fragments/dex/device

    • its /perf//procperf live panels) are scoped the same way, and the DEX device-id lists
      (overview top-devices, per-signal/per-app affected-devices, per-device perf) no longer
      enumerate agents outside the operator's management scope. (Fleet aggregates — rate
      denominators, score histograms — remain fleet-wide statistics.)
  • "Get live info" on the device page. Dispatches read-only instructions to the agent on
    demand (not the 30s heartbeat): Uptime (os_info/uptime) and a running-process list
    (processes/list_hashed) showing the SHA-256 of each process's on-disk executable, with
    a first-10 preview and client-side search. Requires Execution:Execute scoped to the
    device's management group (a device outside your scope cannot be live-queried); each dispatch
    is individually audit-logged (device.live.uptime, device.live.processes) with the
    usage-class read kept separately countable for works-council access audit.

  • REST: agentic-first per-device DEX endpoints. GET /api/v1/dex/devices/{id} — the
    per-device DEX read model (score + signal summary), the machine-readable equivalent of the
    dashboard DEX lens; GuaranteedState:Read scoped to the device, audited dex.device.view,
    off-enum window rejected 400. POST /api/v1/dex/devices/{id}/live?kind=uptime|processes —
    the machine-readable "Get live info": dispatches a read-only instruction now and returns the
    result synchronously (~20s). POST, not GET (it dispatches a command — a side effect);
    GuaranteedState:Read + Execution:Execute scoped to the device; audited per kind
    (device.live.uptime / device.live.processes, audited result=requested pre-dispatch — see
    the Security entry above; the dashboard "Get live info" emitter still audits post-dispatch
    result=dispatched, a tracked alignment follow-up). Concurrent live polls
    are capped server-wide (over-budget → 429); offline → 503, timeout → 504, device error
    → 502, all with retry_after_ms where applicable.

  • processes/list_hashed plugin action (all platforms): proc|pid|name|sha256|path. The
    executable path is resolved from the OS kernel (Windows QueryFullProcessImageNameW, Linux
    /proc/<pid>/exe, macOS proc_pidpath) — not the spoofable argv[0] — and the on-disk image
    is hashed via the shared sha256_file (bounded at 512 MiB, deduped by path).

  • DEX Catalogue is coverage-first. A signal family/type now lights when a connected
    platform collects it — not only when it fired — with per-family "N of M monitored", a
    0–100 family health score, and an OS filter that persists across the family and per-type
    drill-downs. Types no connected platform collects read as not collected, never as healthy.

  • Linux server DEX: systemd unit-health signals from the journal. The Linux DEX
    collector now emits service.hung (a WatchdogSec timeout — the existing
    unit-failed journal entry routed by UNIT_RESULT="watchdog", since a watchdog kill
    is a hang, not a crash; one entry, no double-emit) and os.time_unsynced (chrony's
    "Can't synchronise: no selectable sources" / large-step markers, via a low-volume
    SYSLOG_IDENTIFIER=chronyd query clause; the raw message, which carries NTP source
    IPs, is never shipped). service.hung is verified through the agent's emit on a real
    watchdog timeout. Reused obs_types need no live-render change; the per-OS coverage map
    dex_obs_platforms (dex_routes.cpp) gains the new Linux types. Linux DEX coverage is now 17 signals.
    (service.dependency_failed is deferred — no MESSAGE_ID, and the root failure is
    already captured as service.crashed.)

  • Linux server DEX: /proc + /sys performance/hardware state polls. The Linux
    DEX collector now also emits perf.disk_latency_high (per-I/O service time / iostat
    await over the whole physical disks, from /proc/diskstats — completing the
    Windows cpu/mem/disk perf trio on Linux, same dex_perf_breach hysteresis + 25 ms
    threshold) and hw.cpu_throttled (a poll-and-latch on the /sys
    core_throttle_count thermal-throttle counters, the Windows Kernel-Processor-Power
    37 analogue). New pure parsers parse_diskstats/disk_await_ms/is_whole_disk
    (dex_linux_proc) and parse_throttle_count (new dex_linux_sysfs.cpp); field
    positions verified against a real box. Reused obs_types (no live-render change; the
    per-OS coverage map gains them); this slice adds 2 (Linux 13→15, building on the
    kernel-reliability slice below; the systemd unit-health entry above reaches 17).

  • Linux server DEX: kernel-reliability signals from the journal. The Linux DEX
    collector's journald reader now also classifies _TRANSPORT=kernel ring-buffer
    lines (free text, no MESSAGE_ID) via anchored substring markers in a new pure
    dex_linux_kmsg.cpp, emitting on existing obs_types (no live-render change; the
    per-OS coverage map gains them):
    os.bugcheck (a fatal Kernel panic - not syncing only — survivable Oops/
    soft-lockup deliberately not mapped, to keep the BSOD-equivalent rate honest),
    os.dirty_shutdown (ext4/xfs journal-recovery at mount = the prior shutdown was
    unclean), disk.error (block-layer / buffer I/O error, subject = backing device),
    fs.corruption (ext4/xfs/btrfs metadata error, subject = device), hw.error
    (machine-check / [Hardware Error]), and process.hung (the hung-task watchdog).
    Markers for disk.error, fs.corruption and os.dirty_shutdown are pinned to
    records live-captured on a real systemd-259 box via safe error injection (and
    disk.error verified through the agent's emit on a real injected error); panic, MCE and
    hung-task use the documented kernel format strings. Only infra-safe fields leave
    the device (device / comm / short reason) — the raw kernel MESSAGE is never
    shipped ([dex][linux][kmsg][privacy] pins). Linux server DEX coverage grows from
    7 to 13 reused signals, all in the same /dex display groups.

  • Live-query bundles — one instruction → several plugin actions on one device,
    collated (ADR-0011).
    New POST /api/v1/bundles (dispatch, Execution:Execute,
    returns 202 {bundle_id, agent_id, expected}) + GET /api/v1/bundles/{id}
    (collate, Response:Read, returns {complete, received, succeeded, expected, steps[]} in request order), with MCP parity via the execute_bundle /
    get_bundle_result tools. Collapses the N round-trips of refreshing one device
    (e.g. a ServiceNow CI sync) to a single dispatch plus a poll. The server expands
    the bundle into N ordinary plugin commands under one bundle-… correlation id
    and fans them out async (a slow step never withholds the others); the agent
    is unchanged
    — it never sees a "bundle". bundle_id is deliberately not an
    execution_id (a bundle is not a tracked execution; it is not in the live
    executions drawer). complete means terminal, not success — check
    succeeded == expected (an all-offline bundle completes with succeeded=0). Each
    step emits its own bundle.<plugin>.<action> device-access audit
    (target_type=Agent), so a bundle is exactly as auditable as the N executions it
    replaces; collate enforces an ownership guard (a non-owner gets the same 404 as an
    unknown id). Observable via yuzu_bundle_{dispatched,collated,manifests,evictions}
    metrics plus a yuzu_bundle_dispatch_duration_seconds histogram (labelled by
    surface). v1 bundle state is per-surface and in-memory; a
    durable Postgres manifest for HA + cross-surface collation is a committed
    follow-up (ADR-0011).

  • $Module Windows image-load collector (TAR, M2). The $Module source now
    populates on Windows: a dedicated ETW session on Microsoft-Windows-Kernel-Process
    (IMAGE keyword, image load/unload) captures DLL / driver image loads, with the
    code-signing verdict resolved at drain (WinVerifyTrust + publisher extraction,
    cached by file+mtime) and the loaded-image directory scrubbed of user-profile
    prefixes before storage. Opt-in (module_enabled, default off; enabling takes
    effect on the next collection tick — no restart). Signing uses the local
    Authenticode chain without online CRL/OCSP revocation checking (no per-load
    network I/O), so a revoked driver may read as signed; authoritative
    revoked/blocked detection is the M3 CodeIntegrity overlay. A minimal edge
    risk-filter keeps every unsigned / invalid / revoked / kernel / blocked load at
    full fidelity and dedups + caps normally-signed loads per drain. tar.status reports
    module_capture_method + module_stream_dropped. macOS (Endpoint Security) and
    Linux (auditd kernel modules) collectors follow in M4/M5/M6. See
    docs/tar-module-loads.md.

  • $Module image-load warehouse source — schema foundation (TAR, M1).
    Registers four queryable tables ($Module_Live, $Module_Hourly,
    $Module_Daily, $Module_Monthly) in the TAR warehouse: process/driver
    image-load activity (module name + directory + code-signing verdict) — the
    DLL-search-order-hijack / injection / BYOVD forensic surface that $Process
    cannot answer. Queryable via tar.sql now, empty until a collector lands
    (M2 Windows ETW, M4/M5 macOS Endpoint Security, M6 Linux auditd kernel-module);
    opt-in (module_enabled, default off). The process-stream ring is promoted to
    a reusable EventRing<T> template (no behaviour change for the process
    source), and run_aggregation is now data-driven over the capture-source
    registry so a newly-registered source's rollups can no longer be silently
    omitted. Design + PR ladder: docs/tar-module-loads.md.

  • Gap-free process start/stop capture via Endpoint Security on macOS (TAR).
    The TAR process source on macOS now feeds from the Endpoint Security
    NOTIFY_EXEC/NOTIFY_EXIT stream (was a 60-second KERN_PROC_ALL sysctl
    poll), reaching Windows-ETW parity: short-lived processes the poll missed are
    captured, with accurate timestamps, ppid, image name, and owning user from the
    audit token (names-only — no command line). Requires a full Xcode SDK build
    (the Command Line Tools SDK omits EndpointSecurity.framework), the
    com.apple.developer.endpoint-security.client entitlement, and root; the agent
    automatically falls back to the sysctl poll when any of these is absent, and
    self-heals to the poll if the ES client goes silent for an extended period
    (a NOTIFY-only client has no liveness API — the idle threshold is deliberately
    long to avoid abandoning a healthy stream on a quiet host). The active path is
    reported by tar.status as process_capture_method (endpoint_security or
    polling), alongside two drop counters — process_stream_dropped (userspace
    ring overflow) and process_stream_kernel_dropped (Endpoint Security seq_num
    gaps). The ES handler is noexcept and drops malformed / zero-pid / zero-ts
    events, mirroring the ETW peer. Boot gap: processes that start and exit
    before the agent opens its ES session are not captured — macOS has no
    AutoLogger-equivalent backfill. Fork gap: the stream subscribes
    NOTIFY_EXEC/NOTIFY_EXIT only, so a fork-without-exec child is invisible until
    it exits (NOTIFY_FORK capture deferred, #1455). Both collectors now share a
    cross-platform ProcStreamCollector interface. See docs/user-manual/tar.md and
    docs/darwin-compat.md. Not yet active in shipped builds: the Apple
    entitlement and the codesign/notarization release pipeline are pending
    (#1455), so current macOS agents transparently use the sysctl poll until
    that lands — confirm the live path per device via process_capture_method in
    tar.status (endpoint_security = streaming, polling = fallback).

  • Linux server DEX: os.uptime_report (uptime/reboot heartbeat). The Linux DEX
    collector now emits the hourly os.uptime_report scalar (uptime seconds, from
    /proc/uptime) — the cross-platform reboot/uptime signal (Windows EventLog 6013 /
    macOS boottime equivalent), reusing the same observation builder for an identical
    shape across OSes. Unprivileged, reused obs_type → no server change.

  • Linux server DEX: reliability signals from the systemd journal. The Linux
    DEX collector now reads the journal on a slow cadence (journalctl --after-cursor -o json, cursor-checkpointed) and emits, on the existing obs_types,
    service.crashed (a unit failed — the systemd "Failed with result" / unit-failed
    journal messages),
    process.crashed (a systemd-coredump entry — SD_MESSAGE_COREDUMP; coverage
    depends on systemd-coredump being the active core handler), and memory.exhausted
    (the kernel OOM-killer). Only safe fields leave the device — process comm, unit
    name, signal/result code, never the raw message — and a flapping unit/process is
    collapsed by a per-(type, subject) debounce. No new dependency: journalctl is
    a runtime shell-out (no libsystemd), the source no-ops on non-systemd hosts, and
    the agent account already has systemd-journal/adm read access. Reusing the
    existing obs_types means a Linux server lights up the same /dex buckets as
    Windows/macOS with no server change; obeys the same --dex-disable kill switch.

  • Windows network-quality collection. The agent now emits network facts on
    Windows, not only Linux: device throughput via GetIfTable2 and a system-wide
    interval retransmit rate via GetTcpStatisticsEx (fed through the same
    RetransWindow ΔΣretrans/ΔΣsegs model as Linux). RTT stays unimplemented on
    Windows — per-connection smoothed RTT needs ESTATS
    (GetPerTcpConnectionEStats: enable + admin + overhead), a deferred slice. Two
    honest caveats: the Windows retransmit counter is system-wide (no
    per-interface TCP MIB, so it includes loopback) and is measurement-first,
    unvalidated on Windows
    (the netem separation-under-loss test was Linux-only).
    The /network dashboard now populates on a Windows-only fleet, and Windows
    throughput reaches the yuzu_fleet_net_* gauges. The net gauges carry an os
    label (per-OS, never blended). The Windows retransmit rate is system-wide,
    biased low, and not yet loss-validated, so it is withheld from the
    yuzu_fleet_net_retrans_pct gauge
    (it still shows on the /network page +
    REST, caveated) until #1465 validates it — the gauge carries loss-validated OSes
    only (Linux today). The os gauge label is allowlisted to the values a real
    agent emits (windows/linux/darwin, else other) so an agent-controlled tag
    can't spray unbounded series (the same raw-tag exposure on
    yuzu_fleet_agents_by_arch/by_version is tracked in #1472). See
    docs/user-manual/network.md and docs/user-manual/metrics.md.

  • OS capability matrix (docs/os-capability-matrix.md). A per-capability ×
    per-OS snapshot of what the agent collects/enforces on Windows, Linux, and
    macOS, each row citing its in-code source of truth — so a platform gap (such as
    the previously Linux-only network collector) is visible in one place. Flags the
    durable follow-up: generate the matrix from the existing machine-readable per-OS
    metadata (tar_schema_registry OsSupportStatus, guard support arrays, the DEX
    signal catalogue) rather than hand-maintaining it.

  • Cohort-vs-cohort performance comparison on /dex (F2c). The Performance
    tab's cohort benchmarking compared each cohort against the whole fleet; it now
    also does the direct A-vs-B diff (e.g. image_type vanilla vs layered, or
    model X vs Y) — closing the cohort-vs-cohort half of the benchmarking gap. A
    "Compare two cohorts" section with two cohort pickers auto-loads the top-two
    comparison; each metric shows both cohorts' p50 plus the delta (A relative to
    B). Pure render-time over existing heartbeat state — zero new storage, no
    Postgres. New GET /api/v1/dex/perf/cohort-diff?key=&a=&b= + MCP
    get_dex_perf_cohort_diff (both GuaranteedState:Read, A1 parity with the
    rest of the /dex/perf surface). The fleet-per-app benchmark view (per-app
    perf across the fleet) was deferred at the time of this entry — it has since
    shipped as DEX app-performance-over-time (B1/B2 + the slice-2 read surface;
    see the [Unreleased] entries), reading the retained Postgres aggregate rather
    than the federated device drill. See docs/user-manual/dex.md and
    docs/user-manual/rest-api.md.

  • Network quality dashboard (/network). A new Network view — a sub-view
    under DEX (the Network tab in the DEX sub-nav, also reachable directly at
    /network), not a standalone top-level nav item — surfaces fleet-wide TCP
    network quality measured continuously on each endpoint from kernel counters (no
    packet capture, no flow export) — a device / local-link health view. Fleet-now cards show RTT p50/p90/max, the interval retransmit
    rate
    , and device throughput. The retransmit rate is ΔΣretransmits /
    ΔΣsegments smoothed over the last few heartbeats (recent-window loss), not
    the lifetime ratio — empirically the lifetime ratio is diluted to noise while
    the interval delta cleanly recovers the real loss rate. Measurement-first:
    the page ships honest evidence and does not yet classify a device as
    network-degraded — a calibrated threshold needs real-fleet baseline data, so
    the degraded classification and the device/app co-occurrence headline it gates
    are a later slice (the model stays wired but unfed). Linux agents report
    via netlink INET_DIAG (smoothed RTT + retransmit counters) + /proc/net/dev
    throughput; Windows reports throughput + retransmit (RTT deferred — see the
    Windows entry above); macOS emits nothing yet (absent metrics are omitted,
    never zeroed). Device-aggregate heartbeat tags
    yuzu.net_{rtt_p50_ms,retrans_pct,throughput_bps} (no per-destination data;
    gated by --dex-disable) and Prometheus gauges
    yuzu_fleet_net_{reporting,retrans_reporting,rtt_ms,retrans_pct,throughput_bps}
    (yuzu_fleet_net_retrans_reporting is the retransmit-rate denominator;
    yuzu_fleet_net_degraded is dormant/absent until the classification lands).
    An opt-in per-connection warehouse tier (netqual_enabled TAR config →
    on-device $NetQual_Live, bucket-only destination class, top-N capped, Linux)
    ships as the foundation for the deferred per-destination drill; it has no
    dashboard consumer in v1. Page permission: GuaranteedState:Read. See
    docs/user-manual/network.md and docs/user-manual/metrics.md. Machine-readable
    JSON siblings GET /api/v1/network/fleet and GET /api/v1/network/devices
    (permission GuaranteedState:Read, listed in the OpenAPI spec) and MCP tools
    get_network_fleet / list_network_devices mirror the dashboard data for
    scripting and agentic access (A1 parity).
    Note: yuzu.net_retrans_pct / yuzu_fleet_net_retrans_pct changed meaning
    within the unreleased cycle (absolute lifetime ratio → interval delta) and
    yuzu.net_degraded stopped being emitted — recalibrate any dev-build
    Prometheus alerts built on the earlier 4a semantics.

  • Linux server DEX collector — /proc perf + statvfs storage signals. Linux
    agents now emit perf.cpu_sustained, perf.memory_pressure, and storage.low
    into the DEX pipeline via privilege-light /proc/stat, /proc/meminfo, and
    statvfs reads — reusing the same obs_types, detail_json shape and thresholds as
    Windows/macOS, so they render in the same /dex display groups with zero server or
    dashboard change. Linux CPU% and commit% also feed the perf heartbeat tags
    (yuzu.perf_*). Observe-only; controlled by the existing --dex-disable /
    YUZU_AGENT_DEX_DISABLE flag. Edge-privacy: storage.low subjects are the
    backing-device identifier (e.g. sda1, or the ZFS pool) — never the mount path,
    which can carry usernames/tenant names; pinned by [dex][privacy] regression tests.
    Upgrade note: Linux agents begin emitting this telemetry on upgrade with no
    operator action (EU works-council co-determination triggers on the capability to
    observe); --dex-disable suppresses it fleet-wide.

  • Gap-free process start/stop capture via ETW on Windows (TAR). The TAR
    process source now feeds from a real-time Microsoft-Windows-Kernel-Process
    ETW session instead of the 60 s snapshot-diff poll, so short-lived processes
    (which the poll misses entirely) are captured — with exact timestamps and exit
    codes. Same process_live schema and $Process_* query surface and the same
    started/stopped rollups — but the Windows cmdline column is now empty
    (see Breaking Changes). Events are names-only (no command
    line); the owning user is resolved from the process SID at start (best-effort,
    empty for processes that exit faster than the ~1 s buffer flush — the same
    limit the poll had). The poll remains the feeder on Linux/macOS and the Windows
    fallback if the ETW session cannot start. The boot window (processes that
    start and exit before the agent's live session opens) is backfilled from a
    boot AutoLogger — a circular, FlushTimer-enabled Kernel-Process .etl
    configured by install-agent-user.ps1 (admin, install-time) and started by the
    kernel early each boot; the agent reads it directly at startup for events
    before its live session began (no session stop / no elevation — read access
    only), de-duplicated per boot. Boot-window events are names-only with no user
    (the start event carries only SessionID + integrity label; precise attribution
    would need the Security-Auditing 4688 provider). process_live raw cap raised
    5k→100k for the higher event rate; disk stays bounded (count rollups carry the
    long tail). install-agent-user.ps1 is now UTF-8-BOM-encoded so Windows
    PowerShell 5.1 parses it (it has non-ASCII characters). Disabling the
    process source drains-and-discards the live ETW ring each cycle, so a paused
    window is never persisted on re-enable (forensic-pause contract); the
    boot-backfill replay is memory-bounded and drops corrupt-timestamp / torn
    records.

  • Boot process-capture AutoLogger wired into the Windows installer (#1425).
    The production InnoSetup installer (deploy/packaging/windows/yuzu-agent.iss)
    now configures the YuzuProcBoot boot AutoLogger at install time — scoped to
    the advanced plugin component that ships tar.dll — and tears it down on
    uninstall: it removes the boot config, stops the running trace session, and
    deletes procboot.etl. Stopping the session is load-bearing: Remove-AutologgerConfig
    removes only the boot-start config and leaves a session started by an earlier
    boot running until the next reboot, holding one of the scarce (~64) system ETW
    session slots. The same stop-before-delete teardown was applied to
    install-agent-user.ps1's Remove-ProcBootAutologger so the script and
    installer recipes stay in sync. The installer also locks the agent data dir
    {commonappdata}\Yuzu (and {app}\logs) to admins-full system-full: the
    prior service-full keyword is not a valid InnoSetup permission group and was
    silently ignored, leaving the directory — which now holds the boot trace
    procboot.etl (boot-process names reveal which security/EDR tools are present)
    — readable by authenticated users (matches the yuzu-server.iss data-dir ACL).

  • Account lockout for failed local-password logins (SOC 2 CC6.3). After
    --auth-lockout-threshold (default 5) consecutive failed POST /login
    attempts within --auth-lockout-window-secs (default 900 s) the account is
    locked; subsequent attempts return the same generic 401 as a bad password
    (no username-enumeration / lock-state oracle, and PBKDF2 is skipped). The lock
    auto-expires after the window — it is never permanent — and a waited-out user
    regains a fresh attempt budget. An admin can clear a lock immediately via
    POST /api/v1/users/{username}/unlock (UserManagement:Write, MFA step-up;
    self-target permitted). Set --auth-lockout-threshold=0 to disable. Env vars:
    YUZU_AUTH_LOCKOUT_THRESHOLD / YUZU_AUTH_LOCKOUT_WINDOW_SECS. New audit
    verbs auth.lockout.applied (once, at the threshold crossing) /
    auth.lockout.cleared (successful login or admin unlock); new metrics
    yuzu_auth_lockout_applied_total / yuzu_auth_lockout_blocked_total. Scope is
    local-password login only — OIDC/SSO and API tokens are unaffected.

  • Secrets-at-rest envelope encryption substrate — SecretCodec +
    KekProvider KEK wrap/unwrap seam (ADR-0010, #1320 PR 4; machinery only,
    no store writes secret columns yet).
    Secret columns in PostgreSQL are
    AES-256-GCM-encrypted app-side under a fresh per-value DEK; the DEK is
    wrapped by the install's KEK, which lives behind the KekProvider seam
    (a dedicated interface implemented by FileKeyProvider alongside the CA
    KeyProvider contract)
    (secrets-kek-v<N>.key, 0600, generated on first codec init with a
    temp-fsync-rename atomic write) and never enters the database. Identity-
    bound AAD makes blobs non-relocatable across rows/columns (canonical
    length-prefixed serialization; kek_version rides the wrap layer only, so
    rotation re-wraps DEK headers without touching payloads). The secrets
    schema's kek_meta table registers non-secret KEK fingerprints; boot
    verification fails closed with distinct kek_unresolvable / kek_corrupt
    operator tokens (backup-skew and dual-server misconfigurations refuse
    loudly). KEK lifecycle: rotation (incremental, interruptible, per-row CAS),
    oldest_kek_version_in_use completion signal, retirement refused while a
    version is active or referenced with retired_at destruction evidence.
    Audit verbs kek.generated/kek.rotated/kek.retired/
    secret.decrypt_failure and per-store failure-class counters are defined
    as wiring seams — no audit events or metrics are emitted until the
    per-store migration PRs wire them. Operator guidance (the
    DB+keys-dir restore-pairing invariant, rotation, break-glass) lives in
    docs/user-manual/server-admin.md "Key management (secrets KEK)". The
    gated stores (auth TOTP, webhooks, offload_targets, OIDC client
    secret) adopt the codec as each migrates to Postgres.

  • Guardian: Linux systemd service guard (observe-only). The
    service-status-change Guardian Spark now arms on Linux hosts with systemd,
    watching each unit's ActiveState over sd-bus (Subscribe + PropertiesChanged
    match + a bounded reconcile backstop) and emitting drift.detected events with
    platform=linux, matching the Windows SCM guard's event shape — both service
    guards are silent on the compliant edge (neither emits guard.compliant).
    systemd's richer ActiveState collapses onto the published
    {running, stopped} tokens with no schema change. Observe-only in this
    release:
    drift is detected and reported but not remediated — enforcement
    (mask/stop) is gated behind a forthcoming, governance-reviewed change, and an
    enforcement_mode: enforce rule on a Linux agent degrades to observe without
    error. Non-systemd Linux hosts (no system D-Bus) degrade gracefully: the guard
    does not arm and the rule reports unarmed rather than compliant. The watch
    reconnects automatically after a systemctl daemon-reexec / dbus restart.
    Build/runtime dependency on Linux agents: libsystemd — build libsystemd-dev
    (systemd-devel on RHEL/Fedora), runtime libsystemd.so.0 (libsystemd0 /
    systemd-libs). The default build (-Dsystemd_guard=enabled) requires it so the
    shipped agent always offers the guard — a build that has lost libsystemd fails loudly
    rather than silently shipping a guard-less agent; the shipped Dockerfile.agent,
    Dockerfile.agent.chisel, and the asan/tsan images install it and keep that default.
    Pass -Dsystemd_guard=auto (use if present) or disabled to build the guard as a
    no-op stub on musl/Alpine/non-systemd/minimal images that lack libsystemd.

  • DEX: per-cohort performance gauges for Prometheus/Grafana (opt-in).
    Settings → DEX alerts gains a cohort export tag key: when set (empty by
    default), /metrics publishes yuzu_fleet_perf_cohort_{cpu_pct,commit_pct, disk_lat_ms}{cohort,stat} + yuzu_fleet_perf_cohort_reporting{cohort} for
    that key's cohorts, refreshed on the same sweep (and the same validation
    rules) as the existing fleet gauges. Cardinality is bounded: top 50 cohorts
    by population, the 10-device statistical floor applies, and
    yuzu_fleet_perf_cohort_clipped makes any capping visible rather than
    silent. Devices without the key export as cohort="(untagged)". Changes
    are audit-logged (settings.dex_alerts.cohort_export) and persisted in
    runtime config (dex_cohort_export_key).

  • DEX: device drill-down performance extensions. The /dex per-device
    page gains (1) vs-fleet / vs-cohort percentile strips — the device's
    current heartbeat CPU / memory commit / disk latency placed against the
    current fleet p50–p90 band (marker turns red above fleet p90), with its
    cohort position when the cohort meets the 10-device floor; rendered live
    from registry state, no query dispatched; and (2) a per-application
    panel
    over the device's opt-in $ProcPerf_Hourly edge tier (A2), behind
    its own "Load applications" click with its own audit action
    dex.device.procperf.query
    — usage-class reads stay separately countable
    from machine-health reads. App rows cross-link to the app reliability
    drill. An empty result renders the truthful message (per-app sampling is
    off by default, or no hourly rollup yet — the device's read-only query
    surface deliberately hides plugin config, so the server does not guess
    which).

  • DEX: in-product fleet performance view (/dex → Performance tab). A
    fifth DEX tab answers "where do I watch fleet CPU in the product" without
    Grafana: fleet-now cards (avg/p50/p90/max + the reporting population) for
    CPU utilization, memory commit and disk I/O latency — the same numbers as
    the yuzu_fleet_perf_* Prometheus gauges, computed at render time over
    registry heartbeat state with zero new storage — plus cohort
    benchmarking
    : fleet-relative percentiles per cohort of an operator-chosen
    tag key (e.g. model, image) — fleet-relative percentile
    benchmarking. Honesty rules
    throughout: a metric nobody reported is absent (never 0), cohorts under 10
    reporting devices withhold percentiles ("n too small"), and devices without
    the chosen key appear as an explicit "(untagged)" residual. Every aggregate
    drills to the device list behind it (worst-by-metric, not-reporting,
    cohort members), and each device links on to its drill-down. The read model
    is first-class on all three surfaces: new
    GET /api/v1/dex/perf/{fleet,cohorts,devices} REST endpoints and three MCP
    tools (get_dex_perf_fleet, get_dex_perf_cohorts,
    list_dex_perf_devices), all GuaranteedState:Read-gated. Trend charts
    (retained history) are deliberately deferred to the Postgres-backed series
    store (F2b). Perf telemetry remains Windows-agents-only in this release;
    the population caption carries that scope.

  • DEX: alert routing + tunable blast-radius thresholds (Settings → DEX
    alerts).
    Operators can now route individual DEX signal types to alerts:
    each routed observation raises an operator notification and fires a new
    dex.signal webhook/offload event (obs_type, subject, agent_id),
    once per device per hour per type, with a global per-minute fan-out cap.
    Nothing is routed by default. The same panel makes the fleet blast-radius
    thresholds (min devices / window / cooldown) operator-tunable, applied
    live
    — no restart (memory and fan-out bounds remain fixed; they are DoS
    posture, not policy). Routing is evaluated at the shared observation-ingest
    chokepoint, so directly-connected and gateway-routed agents are covered
    identically. Changes are audit-logged (settings.dex_alerts.*) and
    persisted in runtime config; router activity is observable via the
    yuzu_server_dex_alert_* metrics (fired / delivery_failed / suppressed /
    dropped / cooldowns_evicted / routed_types). The agent-side A3 breach
    thresholds (90 % CPU / 10 min etc.) remain fixed in this release.

  • DEX: per-application resource sampling (TAR procperf warehouse tier).
    Each Windows agent now records, on the same 30 s perf tick, the top 10
    applications by CPU and the top 10 by working set
    (union, ≤ 20 rows/tick)
    into the on-device TAR edge warehouse — $ProcPerf_Live (7-day window) and
    $ProcPerf_Hourly (31-day per-app avg/max rollup). One
    NtQuerySystemInformation snapshot per tick: no PDH, no WMI, no per-process
    handles. Samples are aggregated per image name (12 chrome.exe processes =
    one row, instances=12); cpu_pct is the app's share of total machine
    capacity, matching Task Manager. Privacy: image names only — never
    command lines
    ; TAR redaction patterns apply to the name. Off by default
    (procperf_enabled=false) — per-application data is usage-class telemetry
    subject to works-council/DPA review, distinct from device-level perf (which
    carries no per-app identity and stays on); set procperf_enabled=true to opt
    in, independent of the device-level sampler. Queryable via tar.sql.

  • TAR: warehouse tables are now ensured on every database open. Fixes an
    upgrade bug where tables introduced by a newer release (the perf tier, and
    now procperf) were never created on a pre-existing tar.db — the schema DDL
    only ran on first-time migration, so upgraded agents failed those inserts
    until the database was deleted. The DDL is idempotent (IF NOT EXISTS) and
    now runs on each open.

  • DEX: fleet performance rollup + device perf sparklines. Two new surfaces
    over the device perf telemetry: (1) every Windows agent ships its current
    utilization (CPU busy %, commit-charge % of limit, per-IO disk latency —
    derived over its heartbeat interval) as heartbeat tags, and the server
    aggregates the fleet into Prometheus gauges
    yuzu_fleet_perf_{cpu_pct,commit_pct,disk_lat_ms}{stat="avg"|"p50"|"p90"|"max"}
    plus yuzu_fleet_perf_reporting (the contributing population). Series go
    absent — never a fabricated zero — when nobody reports; non-finite/negative
    values from a rogue agent are rejected and percentages clamp at 100.
    (2) The /dex per-device drill-down gains a device performance panel:
    CPU / memory / disk-latency sparklines over the device's hourly warehouse
    rollups, fetched by a live read-only $Perf_Hourly TAR query dispatched to
    the device when you click Load performance (click-to-load — viewing a
    device page alone never dispatches a command; raw samples stay on-device —
    the federated model). Server-rendered SVG, no JS. Needs the device online
    and Execution:Execute (the panel says so honestly otherwise); each query
    is audit-logged (dex.device.perf.query). Agents with --dex-disable ship
    no perf tags.

  • DEX: sustained performance-breach alerts (Performance signal family). The
    Windows state poller now samples CPU, commit charge, and disk service time
    every 120 s (raw kernel counters — GetSystemTimes, GetPerformanceInfo,
    IOCTL_DISK_PERFORMANCE; no PDH, no WMI, no shell-out) and emits a DEX
    observation when a metric stays bad for a sustained window:
    perf.cpu_sustained (≥90% busy for 10 min), perf.memory_pressure
    (commit charge ≥90% of limit for 10 min), perf.disk_latency_high
    (≥25 ms per IO for 10 min). Each fires once per episode with the window
    average as its metric and re-arms only after a sustained recovery below a
    lower exit threshold (hysteresis — a flapping metric cannot spam the feed;
    worst case ~4 observations/hour/type). The signals ride every existing alert
    surface (SSE, webhooks, blast-radius detection) and land in a new
    "Performance" family on the /dex Catalogue and Health views (display
    catalogue now 107 entries). Thresholds are fixed in this release
    (operator-configurable thresholds are the F1 follow-up). The TAR perf
    warehouse remains the historical record of the same counters; this is the
    alerting leg.

  • RDP control plugin (rdp_control). New Windows-only agent plugin with
    two actions: set_state (enable|disable — writes HKLM fDenyTSConnections,
    toggles the Remote Desktop firewall rule group locale-independently via
    INetFwPolicy2, and starts TermService on enable; per-step status columns
    with overall=ok only when every step succeeds) and status (reads back all
    three gates and derives rdp=on|off|unknown, reporting unknown rather than
    off when a gate is unreadable). Bundled windows.rdp.set_state definition
    (role-gated, endpoint-admin) and windows.rdp.status (auto, question).
    Built for change-gated remote access: an ITSM system can enable RDP at the
    start of an approved change window and disable it afterward. Disable blocks
    new connections only (does not terminate active sessions). Non-Windows builds
    compile to an error-returning stub. Requires the agent service account to be
    in the local Administrators group (not the default install) — see
    docs/agent-privilege-model.md.

  • PostgreSQL deploy prerequisites — native packaging, backup/restore docs,
    UAT sidecar (#1320 PR 2; inert-but-ready, no server behavior change).

    Three deliverables ahead of the substrate's fail-closed flip: (1) the
    provisioning helper install-server-postgres.sh now ships in the server
    .deb/.rpm (at /usr/share/yuzu/scripts/, invoked non-fatally from
    the package post-install hooks) and in the Linux/macOS release tarballs
    (scripts/); (2) operator backup/restore documentation now covers
    PostgreSQL state — pg_dump --format=custom/pg_restore procedures for
    native and Docker deployments in docs/user-manual/server-admin.md (new
    "PostgreSQL Substrate" section) and upgrading.md, including the ADR-0010
    restore-pairing invariant (database and KeyProvider keys-dir backups
    restore together; runbook tracked in #1341) and an explicit warning never
    to tar a live postgres-data volume; (3) the native UAT rig
    (scripts/start-UAT.sh) now stands up a yuzu-postgres:local sidecar
    container on loopback :15433 with per-run random, distinct
    superuser/app-role credentials, exports YUZU_POSTGRES_DSN to the server,
    and tears the container down on stop — a missing docker/sidecar is a
    warning, not a failure, until the #1320 PR 3 fail-closed boot lands
    (PG_SOFT_FAIL flag). The rig's agent-registration poll timeout also
    rises 30s → 60s (the fleet-health gauge needs a first heartbeat + a
    recompute window — measured ~37s; the stack was healthy, the poll was
    just too tight).

  • DEX: continuous device performance sampling (TAR perf warehouse tier).
    Every Windows agent now samples CPU busy %, memory used % + commit-charge %,
    per-IO disk service time (µs) + read/write throughput, and non-loopback
    network rx/tx throughput every 30 s into the on-device TAR edge warehouse
    ($Perf_Live, 7-day window; $Perf_Hourly avg/max rollup, 31-day) — raw
    samples never cross the wire (ADR-0004 federated model). Raw kernel counters
    only (GetSystemTimes, GlobalMemoryStatusEx/GetPerformanceInfo,
    IOCTL_DISK_PERFORMANCE, GetIfTable2); no PDH, no WMI, no shell-out. New
    tar collect_perf action and configure keys perf_enabled (default
    true) and perf_interval_seconds (default 30; 0 disables the trigger
    entirely). Queryable via tar.sql. Linux (/proc) and macOS
    (host_statistics) collectors are planned. Threshold-breach alerting shipped
    alongside (see "sustained performance-breach alerts" above); fleet rollup
    (A4) is a follow-on slice — today's data is collection + raw-SQL query +
    breach alerts, no dashboard chart yet.

  • DEX: fleet blast-radius incident alerting. When ≥5 distinct devices report
    the same DEX signal (obs_type, subject) within a 15-minute sliding window,
    the server raises an operator notification (severity warn) and fires a new
    dex.blast_radius webhook/offload event (payload: obs_type, subject,
    device_count, window_seconds) — subscribe to auto-open ITSM incidents.
    Detection runs at the shared Guardian ingest chokepoint, so directly-connected
    and gateway-routed agents both contribute; the window uses server receipt time
    (clock-skew tolerant). Per-pair 1-hour re-alert cooldown; a global
    per-minute fan-out cap and a bounded LRU pair map keep it kind to the server
    and the ITSM sink under a correlated multi-subject incident. Metrics:
    yuzu_server_dex_blast_radius_{incidents,fires_dropped,entries_dropped,pairs_evicted}_total

    • yuzu_server_dex_blast_radius_pairs_tracked. The 5-devices / 15-min / 1-h
      defaults are now operator-tunable under Settings → DEX alerts (see the alert
      routing entry above).
  • DEX: Windows disk-space and battery-health observations. A Windows
    state-poll collector (dex_win_poll) emits storage.low (fixed volume ≥90%
    full or <5 GiB free, 10-min cadence, via GetDiskFreeSpaceExW) and hw.error
    (battery full-charge capacity <80% of design, hourly, via IOCTL_BATTERY)
    on the transition into a bad state (latched; re-arms only on a valid healthy
    reading). Thresholds match the macOS IOKit poll; zero server change.
    storage.low and battery (hw.error) are now emitted on both Windows and
    macOS (final display-catalogue count for this release: see the Performance
    family entry above).

  • Network probes (netprobe plugin). Active RTT, jitter, and packet-loss
    measurement to operator-chosen targets using native system calls (no
    shell-out): network.probe.icmp (ICMP echo — IcmpSendEcho on Windows,
    unprivileged ICMP datagram sockets on POSIX; on Linux gated by
    net.ipv4.ping_group_range, reporting not-permitted rather than fake loss),
    network.probe.tcp (connect-time RTT to a target:port, sub-millisecond,
    unprivileged everywhere — use where ICMP is dropped), and network.probe.dns
    (getaddrinfo resolution timing). Bounded at 4 targets × 10 samples × 3 s,
    sequential. Recurrence via the scheduler; results trend in the response store.
    Admin-only execution (network-reconnaissance primitive).

  • Hardware: installed-driver inventory. New hardware drivers action and
    device.hardware.drivers definition list installed device drivers (name,
    version, date, provider, class) via Win32_PnPSignedDriver on Windows and
    /proc/modules on Linux — supports fleet queries like "devices running driver
    X older than version Y".

  • macOS DEX collector — limited, unprivileged. The DEX observer now ships a
    macOS collector (dex_macos_collector.cpp + the pure parsers
    dex_macos_{signals,oslog,iokit}.{hpp,cpp}) using four privilege-light
    mechanisms — a kqueue DiagnosticReports folder-watch (.ips/.diag
    crashes/hangs/panics/jetsam), a sysctl(KERN_BOOTTIME) uptime scalar, an
    incremental log show unified-log poll, and an IOKit/system-tool poll
    (SMART/battery/disk/thermal/memory). It reuses the OS-neutral obs_types, so no
    server or dashboard change is needed beyond one macOS-only type — storage.low
    (disk nearly full) — bringing the catalogue to 104. Covers ~10 of 11 DEX
    experience headings unprivileged; the Security heading (XProtect/Gatekeeper) is
    not yet shipped. Privacy is minimised at the edge (faulting-image basenames
    only, never raw log message bodies; per-type rate caps; non-finite metric
    guards). Obeys the same --dex-disable kill switch as Windows. Source mapping +
    the Endpoint-Security entitlement roadmap: docs/dex-signal-catalog.md.

  • DEX dashboard (/dex) — a hub plus three deep pages. The read-only fleet-
    reliability lens over the Guardian observation catalogue is structured as a
    hub (measured crash-free-device % and crashes/1k-device-days, honest "—"
    when no agents report) that summarises and links into three deep pages:
    Catalogue (all 104 monitored signal types across 12 families — App
    reliability, Boot/start-up & shutdown, Service health, System stability,
    Hardware & storage, File system, Network, Identity & logon, Security &
    protection, Updates & installs, Policy & management, Printing — quiet types as
    real-zero rows, with per-type drill-down and an "Other" bucket for uncatalogued
    types a newer agent emits), Health score (a transparent, secondary 0–100
    composite — 100 − Σ weighted deductions — suppressed, never fabricated, when
    no agents report), and Trends (cross-OS comparison cards carrying each OS's
    live signal scope, per-family sparklines, and a guard×day activity heatmap). A
    24h/7d/30d/All window selector applies to every view; per-app and per-device
    drill-downs (and the per-signal device list) are permission-gated
    (GuaranteedState:Read) and audit-logged on open (behavioral data). A "DEX"

… 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 /SERVER=yuzu-server:50051 /TOKEN=<enrollment-token>

# Server
YuzuServerSetup-*.exe /VERYSILENT /ADMIN_USER=admin /ADMIN_PASS=<password> `
  /HTTPS_CERT=cert.pem /HTTPS_KEY=key.pem

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/Tr3kkR/yuzu-server:v0.13.0
docker pull ghcr.io/Tr3kkR/yuzu-gateway:v0.13.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}-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.