Repository navigation
Yuzu v0.13.0
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_schedulehad no production caller — schedules persisted and listed but never dispatched. A newScheduleRunnerpoller (thePolicyEvaluator/PreflightRunnershape, 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_approvalOR definitionapproval_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/schedulesrequires BOTHSchedule:WriteandExecution: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 requiresExecution:Executetoo (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,
andquarantine_device(records the quarantine and dispatches live isolation).
Approval-gated tools use a ticket-then-recall flow: the first call returns
kApprovalRequired(-32006) withapproval_id+status_url; after a second
principal approves, the caller re-invokes with theapproval_idand 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_deviceisSecurity:Executeon BOTH
transports and the supervised approval-policy rule moved with it — closing a mismatch
that let a supervised token quarantine via RESTPOST /api/v1/quarantinewith 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 recordsconsumed_by
(SOC 2 CC7.2 evidence chain: submitted_by → reviewed_by → consumed_by); and the
device-targeted ticket-mint audit rows carryagent_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 catalogversion: 2) enriches each action with itsparameter_schemainline
when the action has a published InstructionDefinition (joined on plugin+action), and
reportsactions_enriched_with_schema— so a worker learns how to call an action,
not just that it exists, from one request. Theexecute_instructionand
discover_pluginstool descriptions now spell out the async dispatch→poll pattern and
point at theyuzu://operating-modelresource. 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_urltarget); theauth_routesdenial shapes are unified into one A4
envelope carryingcorrelation_idand the missingsecurable_type:operation; the
~156 legacyerror_jsonsites inrest_api_v1.cppnow emit the A4 envelope.
Breaking — many of those/api/v1errors were previously{"error":"<string>"}
and are now the nested A4 object{"error":{"code","message","correlation_id",…}};
a client that readerroras a string will break. Migrate toerror.code/
error.message. Seedocs/user-manual/upgrading.md. -
Installed-software inventory gains package-manager fields (blob contract v2, ADR-0016).
Every row inGET /api/v1/inventory/softwareand thequery_installed_softwareMCP tool
now carrieskind(package|app),ecosystem(rpm|deb|apk|pacman|windows|macos|homebrew),
epoch,release,arch,signature_status(rpm only, from stored rpm header tags —
never a liverpm -Kverification),distro_id, anddistro_version, alongside the
original name/version/publisher/install_date. A field the ecosystem does not store is
"", never synthesised — seedocs/user-manual/inventory.mdfor the full per-ecosystem
matrix. Collection is a newinstalled_appsplugin action,list_inventory; the
operator-facinglist/query/list_per_useractions are unchanged. Newapk
(Alpine) package enumeration on the sync path. Breaking — data shift on Linux
rpm fleets:publishernow reflects the rpm PACKAGER tag (was VENDOR), andversion
is upstream-only with the release moved to its own column (was fused
VERSION-RELEASE). A saved query/dashboard filter against the oldpublisheror
fusedversionshape on rpm hosts will silently stop matching post-upgrade — see
docs/user-manual/inventory.mdbefore 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 activatePOST /api/v1/elevate
when their SSO session was authenticated with IdP MFA — asserted via the
OIDCamrclaim and seeded intoSession::mfa_verified_atat
/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 auserstable 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 merelyauth_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-elevationdisables 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.grantedaudit detail records the factor source
(duration_secs=<n> mfa=<oidc_amr|local_totp> expires_at=<rfc3339> justification=<text>
— the machine-parsedmfa=andexpires_at=tokens are placed before the
free-textjustification=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 manualPOST /api/v1/elevate/revoke) is now audited as
role.elevation.expired— emitted LAZILY (no new background thread) by
AuthManager::reap_expired_elevation, called from theAuthRoutes::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 spuriousexpired
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 asexpires_inplus a new
wall-clockexpires_at(RFC3339 UTC), and therole.elevation.grantedaudit detail
carriesexpires_attoo. -
SAML 2.0 SP — thin first slice. SP-initiated login against a single, statically-pinned IdP.
HTTP-Redirect binding for theAuthnRequest; 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;InResponseTois 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-authzgap-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 reviewdocs/security-reviews/saml-sp-2026-07-01.md. -
/inventoryDevices tab shows real device-CI data. The Serial/Model/CPU-RAM
columns (previously greyed placeholders) now read fromDeviceInventoryStore, 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 extendedinventory.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 auditsresult=failure(not justsuccess) 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 asinstalled_software. Thedevice_cisource fans out to the
hardware,device_identity,os_info, andnetwork_configplugins 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 newhardwarepluginsystemaction collects serial + system
UUID (Windows WMI / Linux sysfs-DMI viacap_dac_read_search/ macOSioreg); it is
also dispatchable on-demand via the audited instruction path. Server:
DeviceInventoryStore(born-on-PG, schemadevice_inventory_store, one row per agent;
ingest viadevice_ci_ingestion.*on both the directReportInventoryand gateway
ProxyInventorypaths).last_seen/first_seenare the server receipt time (#1685).
Suppressed by--inventory-disablealong 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/inventoryDevices tab bullet
above. -
/autoVERIFY — before/after application-performance evidence (UAT non-functional). A third
stage on the/autopage (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 (allGuaranteedState:Read): RESTGET /api/v1/dex/perf/compare, MCP
compare_app_perf_versions, and the/autodashboard VERIFY stage (aggregate cards + distribution- an audited per-machine drill). Pure engine
app_perf_compare(reducer +comparesplit so a
later live candidate plugs the same slot); B1 cohort readapp_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.
- an audited per-machine drill). Pure engine
-
TAR retention-paused source purge (Phase 15.A). The
/tarretention-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 actiontar.purge_source(refuses unless the source is paused —
the authoritative TOCTOU guard, held atomically with the delete under the collector lock), dashboard
fragmentPOST /fragments/tar/retention-paused/purgeand an agentic-first REST endpoint
POST /api/v1/tar/retention-paused/purge— both gated onInfrastructure:Delete(per-device scoped),
a typed-hostname confirmation (nativeprompt(), CSP-safe) + CSRF same-site gate on the fragment,
audit verbtar.source.purge, and metricyuzu_tar_source_purge_total{result}. The generic
POST /api/commandpath also elevatestar.purge_sourcetoInfrastructure:Delete+ visible-agent
confinement so it can't be a weaker route. The purge also forces awal_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: thetar.purge_sourceaction requires an agent at this release or later; an older
agent returnserror|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-userusers.elevation_eligibleflag, auth.db
migration v5, admin-managed viaPOST /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'seffective_role()is admin only while elevated;require_adminand 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/revokefor 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-authzgap-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-reservedsessions.last_activity_atcolumn end-to-end (enforced in
AuthManager::validate_session; best-effort throttledauth.dbmirror via
AuthDB::touch_session_activity). Default 0 = disabled (opt-in; existing deployments unaffected;
recommended900= 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-authzgap-matrix P1 #8. Seedocs/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 viayuzu_auth_local_disabled_total(metric,
not a per-attempt audit row). A single--break-glass-useris exempt only while armed, armed
out-of-band via the host CLIyuzu-server --break-glass-arm(auto-expiring, default 24h via
--break-glass-window-secs; auditedauth.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-denied403, never offered enrollment). Use is audited atSeverity::kCritical
(auth.breakglass.login/auth.breakglass.denied) + metricyuzu_auth_break_glass_login_total.
AuthDB migration v4 adds a nullableusers.break_glass_armed_untilcolumn (additive, data-safe).
Opt-in: existing deployments default tostandardand are unaffected. Enablingsso-only
refuses to start without--oidc-issuerconfigured and (if set) a--break-glass-userthat
exists and has MFA enrolled — configure OIDC and pre-enroll the break-glass account before
restarting. Seedocs/auth-architecture.md"Hardened mode",docs/user-manual/upgrading.md, and the
docs/ops-runbooks/auth-db-recovery.mdbreak-glass arm runbook. Closes/auth-and-authzP0 #3. -
/autodeploy — stage + execute an upgrade on a pre-flight go-cohort. The/autopage
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 existingcontent_distplugin (stage/
execute_staged); no new agent code. Born-on-PostgresDeploymentRunStore(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_execcommitsstaged→executingbefore 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 humanconfirm()gates the deploy. RBAC:
SoftwareDeployment:Readopens the config; the result poll needsRead+Execute(it advances
the engine); create needsInfrastructure:Read+Execute; delete needsExecute; owner-scoped;
operationaldeployment.{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 backgroundSoftwareCatalogRollupthread, keep-last-good on
failure) so page reads never run a full-tableGROUP 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 existingInventory: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, andinventory.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 (RESTGET /api/v1/dex/devices/{id}/app-perf, shipped in slice 2) now has a dashboard
surface: an "Application performance over time" panel on the/deviceDEX drill, beside the
live Top-applications query. It reads the central B1 store (no live device query, no
Executepermission — 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 samedex.device.app_perf.viewaudit verb as the REST drill,
under the dashboard set-and-proceed posture (setsSec-Audit-Failedbut still renders, distinct
from the REST fail-closed surface). A new pure reductionapp_perf_device_summaries(in the
shareddex_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 (allGuaranteedState: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-deviceGET /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 belowkDexCohortFloor(10) devices to a count only — a sub-floor
aggregate singles out an individual even without anagent_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-Failedif 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 sourceapp_perf(agent) rolls the on-deviceprocperf_hourlywarehouse 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 existingReportInventory
transport as a newplugin_datakey (no proto change, no gateway regen). New born-on-Postgres
AppPerfDailyStore(schemaapp_perf_daily_store, 31-day retention, plain table + per-agent
prune) persists them via a shared ingest seam wired identically on the directReportInventory
and gatewayProxyInventorypaths, 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 samecanon_versionthe 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 byprocperf_enabled+ the--inventory-disabledaily-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(schemaapp_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 byAppPerfRollup— the ADR-0012 cross-store query owner — as one
server-sideINSERT … SELECT … ON CONFLICTper day (histogram viaCOUNT(*) FILTERover
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_versionstamps 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 noagent_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(schemapreflight_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(gatesInfrastructure:Read+Execution:Execute)
andpreflight.run.delete(gatesExecution:Execute). Reached by URL — not a nav tab. -
disk_spaceagent plugin — cross-platform free-space probe. New plugin (Windows/Linux/macOS),
singlefreeaction: returns total bytes, free bytes, and percent used for a volume (pathparam;
defaultC:\//). Free is quota-aware caller headroom (FreeBytesAvailableToCalleron Windows,
f_bavailon POSIX). Content definitioncrossplatform.storage.free. -
Device page — live Disk space card. New card on the device "Get live info" lens dispatches
disk_space/freeon demand; per-volume table with a colour-coded usage bar (>=90% used or <5 GiB
free = red, matching thestorage.lowDEX threshold). Audit verbdevice.live.disk. -
REST endpoint for fleet-wide installed-software inventory (ADR-0016 follow-on).
GET /api/v1/inventory/softwareexposes the typedSoftwareInventoryStoreover REST
(gated onInventory:Read), the agentic-first sibling of thequery_installed_software
MCP tool. Filter byname/agent_id(omit both for a fleet scan);limitcapped at
1000 withresult_truncated_by_capwhen more rows exist. A per-agent management-group
drop filter is applied — out-of-scope devices are dropped and counted indevices_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:Readgate (ADR-0017; seedocs/user-manual/inventory.md§Scope),
with a distinct scope-denied audit row. On store degradation it returns503(an A4
error envelope), never an empty200, 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 overReportInventory, 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 repliesneed_fullto 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 newInventoryRBAC securable (Inventory:Read).
Reuses the existinginstalled_appsplugin (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 consecutiveneed_fullresends (so a server cold-cache or store
outage cannot drive a flat-cadence full-resend storm), ayuzu_inventory_ingest_total{source,outcome}
metric, and the store wired into both/readyzand/healthz. Readable now via the
query_installed_softwareMCP tool (Inventory:Read, filter by name/agent, management-group
scoped so an operator sees only their own devices) — distinct from the genericquery_inventory
tools, and viaGET /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 dedicateddropped-outcome alert (it never self-heals); and
query_installed_softwarereportsdevices_omittedso 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 silentsuccesswith 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 viasoftware_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 withtar.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 aninstalledevent always means
"installed now".tar.statusreports asoftware_last_run_tsheartbeat. On upgrade,
tar.statusgains asoftware_*block plus thesoftware_interval_seconds/
software_last_run_tslines
— 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).SystemComponententries are
read asREG_DWORD(their canonical type) so system components/patches are correctly
excluded, andsoftwarerows are reachable from the legacytar.query/tar.export
typed paths as well astar.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 (so10.0
outranks9.0— a lexicographic compare previously suppressed real upgrades).tar.snapshot
now collects the source too. Seedocs/user-manual/tar.mdand the data-handling
classification indocs/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):PowerButtonTimestampnow discriminates a held power
button (hard-hang recovery / manual power-off) from a sudden supply loss. Adds a
SignalObservation::suppressprimitive (an extractor can veto a matched benign
event). Full signal table indocs/dex-signal-catalog.mdWave 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
existingdex_top_appsaggregation — 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.
NewGET /api/v1/guaranteed-state/device-compliance?baseline={name}&agent_id={id}
looks a Baseline up by name (a stable constant such asServiceNow Compliance, not
a churningbaseline_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 alast_updatedfreshness stamp. One Baseline carries a
superset of Guards, each scoped viascope_expr, so the push arms a different
subset per machine; the endpoint returns thedeployed_snapshotintersected with the
Guards the device has reported, so an out-of-scope Guard is absent (notpending)
and two machines querying the same Baseline name legitimately see different Guards.
total_guardsis that applicable count. A not-yet-deployed Baseline returns
deployed:falsewith empty guards (consumer renders "No Baseline Deployed"), and
member edits appear only after a re-deploy. A deployed Baseline returning
total_guards:0withlast_updated:nullis 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 render0/0as green.
Designed for embedding Guardian compliance into an external CMDB / ITSM CI record
(e.g. ServiceNow).
Authorization is per-device-scopedGuaranteedState:Read(a global grant passes
fleet-wide; a management-group-scoped principal must holdReadvia a group the
device is in — a previously group-scoped token now gets403for out-of-scope
devices, where a flat global check would have passed them; global tokens are
unaffected). Auditedguardian.device.viewon access (denials audited at the auth
layer asauth.scoped_permission_required). A behavioural-PII read, so it fails
closed: if theguardian.device.viewaudit row cannot persist it returns503+
Sec-Audit-Failed: trueand withholds the compliance body (parity with
GET /api/v1/dex/devices/{id}, governance #1549) — the503is 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
503where this unreleased route previously returned200; 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; the400/404/503bodies use the A4 envelope
(correlation_id), while the403is 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-readableassessableflag (falsefor a draft
Baseline or a deployed Baseline this device has reported no applicable Guard against —
the consumer must not compute a0/0compliance %) andsnapshot_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, notpending), so consumers must not gate on
compliant/total_guardsuntil the per-devicescope_exprcomputed denominator lands
(deferred).X-Correlation-Idis set on every response (parity with the
dex.device.viewsiblings); a guard reported with an unrecognized verdict token keeps
its realupdated_at(no longer suppressed); a baseline-store fault returns a
retryable503(not the404a CMDB would read as "delete this CI") on a transient
fault; and the guard-name lookup chunks itsIN-list so a Baseline larger than
SQLITE_MAX_VARIABLE_NUMBERstill resolves every name. AYuzuAuditPersistFailures
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(WindowsGetIpNetTable2) — these ship with agents built from
this release onward; an older agent renders an empty Processes-tree / ARP card (it
returns anunknown actionresponse, 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. RequiresExecution:Execute+GuaranteedState:Read, scoped to the device. The
previous flat "Running processes" panel (kind=processes) is retained for
REST/scripted callers; the dashboard now dispatcheskind=process_tree. REST/scripted
callers reachkind=uptime|processesonly — the 9-card grid is dashboard-only pending
the A1 JSON backfill (#1649). Seedocs/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
viaGetIpNetTable2→$ARP_Live/$ARP_Hourly) anddns(device DNS
resolver-cache state viaDnsGetCacheDataTable→$DNS_Live/$DNS_Hourly—
device-level, no per-process attribution; DNS is usage-class PII, enabling is
audited). Queryable viatar.sql, thequery/exportactions (type: arp|dns),
and thecrossplatform.tar.recent_arp/recent_dnscanned instructions; toggled
viacrossplatform.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 verbstar.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 linksdnsapi; the resolver-cache
read combinesDNS_QUERY_NO_WIRE_QUERYwith the additional cache-read flag required
to surface cached records on current Windows 10/11 builds. -
MCP
query_responsescloses the dispatch→collect loop byexecution_id. The tool now
accepts anexecution_idargument (routed toResponseStore::query_by_execution) so an
agentic worker that dispatched viaexecute_instructioncan collect exactly that run's
responses — exact-correlation, no cross-execution bleed — instead of the definition-wide
instruction_idcollect. At least one ofexecution_id/instruction_idis now required (was
instruction_id-only); when both are given,execution_idwins. Each returned row now echoes
itsexecution_id. The advertisedstatusfilter is corrected tointegerin the tool schema
(the handler always read it as theCommandResponsestatus enum; the priorstringdeclaration
was wrong), andlimitis now clamped to[1,1000](a negative limit previously bound as an
unbounded SQLiteLIMIT -1, andlimit:0returned 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 siblingaggregate_responsestool still keys on
instruction_idonly; anexecution_idaggregate is a follow-up. MCP-first: the streaming REST
surfaceGET /api/v1/events?execution_id=already exists; a non-streaming polling REST collect
byexecution_idis 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 prefixesauth.,
mfa.,session.) over an optional epoch-seconds window — a representative
sample for auditor evidence rather than the latest-N. Gated onAuditLog: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 newAuditQueryknobs
(action_prefixesOR-group filter +random_sampleordering); discoverable
in the OpenAPI spec (A2). Closes the P0 auth gap "sampled auth-log evidence
export". -
Guardian ingest-drop counter. New
/metricscounter
yuzu_server_guardian_events_dropped_totalcounts cumulative Guardian events dropped at
ingest on anevent_idPK/UNIQUE conflict (redelivery, an agentevent_seq_reset, clock
skew, or a forged-id pre-claim).> 0distinguishes "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/fleetinstead 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 requiresInfrastructure:Read(parity with/api/agents, via the same
get_visible_agents_jsonprovider — 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 viarequire_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//procperflive 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.)
- its
-
"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. RequiresExecution:Executescoped 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:Readscoped to the device, auditeddex.device.view,
off-enumwindowrejected400.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:Executescoped to the device; audited per kind
(device.live.uptime/device.live.processes, auditedresult=requestedpre-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 withretry_after_mswhere applicable. -
processes/list_hashedplugin action (all platforms):proc|pid|name|sha256|path. The
executable path is resolved from the OS kernel (WindowsQueryFullProcessImageNameW, Linux
/proc/<pid>/exe, macOSproc_pidpath) — not the spoofable argv[0] — and the on-disk image
is hashed via the sharedsha256_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 emitsservice.hung(aWatchdogSectimeout — the existing
unit-failed journal entry routed byUNIT_RESULT="watchdog", since a watchdog kill
is a hang, not a crash; one entry, no double-emit) andos.time_unsynced(chrony's
"Can't synchronise: no selectable sources" / large-step markers, via a low-volume
SYSLOG_IDENTIFIER=chronydquery clause; the raw message, which carries NTP source
IPs, is never shipped).service.hungis 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_failedis deferred — noMESSAGE_ID, and the root failure is
already captured asservice.crashed.) -
Linux server DEX:
/proc+/sysperformance/hardware state polls. The Linux
DEX collector now also emitsperf.disk_latency_high(per-I/O service time / iostat
awaitover the whole physical disks, from/proc/diskstats— completing the
Windows cpu/mem/disk perf trio on Linux, samedex_perf_breachhysteresis + 25 ms
threshold) andhw.cpu_throttled(a poll-and-latch on the/sys
core_throttle_countthermal-throttle counters, the Windows Kernel-Processor-Power
37 analogue). New pure parsersparse_diskstats/disk_await_ms/is_whole_disk
(dex_linux_proc) andparse_throttle_count(newdex_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=kernelring-buffer
lines (free text, noMESSAGE_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 fatalKernel panic - not syncingonly — 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]), andprocess.hung(the hung-task watchdog).
Markers fordisk.error,fs.corruptionandos.dirty_shutdownare pinned to
records live-captured on a real systemd-259 box via safe error injection (and
disk.errorverified 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 kernelMESSAGEis never
shipped ([dex][linux][kmsg][privacy]pins). Linux server DEX coverage grows from
7 to 13 reused signals, all in the same/dexdisplay groups. -
Live-query bundles — one instruction → several plugin actions on one device,
collated (ADR-0011). NewPOST /api/v1/bundles(dispatch,Execution:Execute,
returns202 {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 theexecute_bundle/
get_bundle_resulttools. 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 onebundle-…correlation id
and fans them out async (a slow step never withholds the others); the agent
is unchanged — it never sees a "bundle".bundle_idis deliberately not an
execution_id(a bundle is not a tracked execution; it is not in the live
executions drawer).completemeans terminal, not success — check
succeeded == expected(an all-offline bundle completes withsucceeded=0). Each
step emits its ownbundle.<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 viayuzu_bundle_{dispatched,collated,manifests,evictions}
metrics plus ayuzu_bundle_dispatch_duration_secondshistogram (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). -
$ModuleWindows image-load collector (TAR, M2). The$Modulesource 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 assigned; 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.statusreports
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. -
$Moduleimage-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 viatar.sqlnow, 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 reusableEventRing<T>template (no behaviour change for theprocess
source), andrun_aggregationis 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 TARprocesssource on macOS now feeds from the Endpoint Security
NOTIFY_EXEC/NOTIFY_EXITstream (was a 60-secondKERN_PROC_ALLsysctl
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 omitsEndpointSecurity.framework), the
com.apple.developer.endpoint-security.cliententitlement, 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 bytar.statusasprocess_capture_method(endpoint_securityor
polling), alongside two drop counters —process_stream_dropped(userspace
ring overflow) andprocess_stream_kernel_dropped(Endpoint Securityseq_num
gaps). The ES handler isnoexceptand 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_EXITonly, so a fork-without-exec child is invisible until
it exits (NOTIFY_FORKcapture deferred, #1455). Both collectors now share a
cross-platformProcStreamCollectorinterface. Seedocs/user-manual/tar.mdand
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 viaprocess_capture_methodin
tar.status(endpoint_security= streaming,polling= fallback). -
Linux server DEX:
os.uptime_report(uptime/reboot heartbeat). The Linux DEX
collector now emits the hourlyos.uptime_reportscalar (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(asystemd-coredumpentry —SD_MESSAGE_COREDUMP; coverage
depends on systemd-coredump being the active core handler), andmemory.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:journalctlis
a runtime shell-out (no libsystemd), the source no-ops on non-systemd hosts, and
the agent account already hassystemd-journal/admread access. Reusing the
existing obs_types means a Linux server lights up the same/dexbuckets as
Windows/macOS with no server change; obeys the same--dex-disablekill switch. -
Windows network-quality collection. The agent now emits network facts on
Windows, not only Linux: device throughput viaGetIfTable2and a system-wide
interval retransmit rate viaGetTcpStatisticsEx(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/networkdashboard now populates on a Windows-only fleet, and Windows
throughput reaches theyuzu_fleet_net_*gauges. The net gauges carry anos
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_pctgauge (it still shows on the/networkpage +
REST, caveated) until #1465 validates it — the gauge carries loss-validated OSes
only (Linux today). Theosgauge label is allowlisted to the values a real
agent emits (windows/linux/darwin, elseother) so an agent-controlled tag
can't spray unbounded series (the same raw-tag exposure on
yuzu_fleet_agents_by_arch/by_versionis tracked in #1472). See
docs/user-manual/network.mdanddocs/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_registryOsSupportStatus, 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_typevanilla vs layered, or
modelX 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. NewGET /api/v1/dex/perf/cohort-diff?key=&a=&b=+ MCP
get_dex_perf_cohort_diff(bothGuaranteedState:Read, A1 parity with the
rest of the/dex/perfsurface). 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. Seedocs/user-manual/dex.mdand
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 netlinkINET_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_reportingis the retransmit-rate denominator;
yuzu_fleet_net_degradedis dormant/absent until the classification lands).
An opt-in per-connection warehouse tier (netqual_enabledTAR 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.mdanddocs/user-manual/metrics.md. Machine-readable
JSON siblingsGET /api/v1/network/fleetandGET /api/v1/network/devices
(permissionGuaranteedState:Read, listed in the OpenAPI spec) and MCP tools
get_network_fleet/list_network_devicesmirror the dashboard data for
scripting and agentic access (A1 parity).
Note:yuzu.net_retrans_pct/yuzu_fleet_net_retrans_pctchanged meaning
within the unreleased cycle (absolute lifetime ratio → interval delta) and
yuzu.net_degradedstopped being emitted — recalibrate any dev-build
Prometheus alerts built on the earlier 4a semantics. -
Linux server DEX collector —
/procperf +statvfsstorage signals. Linux
agents now emitperf.cpu_sustained,perf.memory_pressure, andstorage.low
into the DEX pipeline via privilege-light/proc/stat,/proc/meminfo, and
statvfsreads — reusing the same obs_types,detail_jsonshape and thresholds as
Windows/macOS, so they render in the same/dexdisplay 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_DISABLEflag. Edge-privacy:storage.lowsubjects 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-disablesuppresses it fleet-wide. -
Gap-free process start/stop capture via ETW on Windows (TAR). The TAR
processsource now feeds from a real-timeMicrosoft-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. Sameprocess_liveschema and$Process_*query surface and the same
started/stoppedrollups — but the Windowscmdlinecolumn 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 byinstall-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_liveraw cap raised
5k→100k for the higher event rate; disk stays bounded (count rollups carry the
long tail).install-agent-user.ps1is now UTF-8-BOM-encoded so Windows
PowerShell 5.1 parses it (it has non-ASCII characters). Disabling the
processsource 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 theYuzuProcBootboot AutoLogger at install time — scoped to
theadvancedplugin component that shipstar.dll— and tears it down on
uninstall: it removes the boot config, stops the running trace session, and
deletesprocboot.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'sRemove-ProcBootAutologgerso the script and
installer recipes stay in sync. The installer also locks the agent data dir
{commonappdata}\Yuzu(and{app}\logs) toadmins-full system-full: the
priorservice-fullkeyword 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 theyuzu-server.issdata-dir ACL). -
Account lockout for failed local-password logins (SOC 2 CC6.3). After
--auth-lockout-threshold(default 5) consecutive failedPOST /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=0to disable. Env vars:
YUZU_AUTH_LOCKOUT_THRESHOLD/YUZU_AUTH_LOCKOUT_WINDOW_SECS. New audit
verbsauth.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+
KekProviderKEK 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 theKekProviderseam
(a dedicated interface implemented byFileKeyProvideralongside the CA
KeyProvidercontract)
(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). Thesecrets
schema'skek_metatable registers non-secret KEK fingerprints; boot
verification fails closed with distinctkek_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_usecompletion signal, retirement refused while a
version is active or referenced withretired_atdestruction evidence.
Audit verbskek.generated/kek.rotated/kek.retired/
secret.decrypt_failureand 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 (authTOTP,webhooks,offload_targets, OIDC client
secret) adopt the codec as each migrates to Postgres. -
Guardian: Linux systemd service guard (observe-only). The
service-status-changeGuardian Spark now arms on Linux hosts with systemd,
watching each unit'sActiveStateover sd-bus (Subscribe+PropertiesChanged
match + a bounded reconcile backstop) and emittingdrift.detectedevents with
platform=linux, matching the Windows SCM guard's event shape — both service
guards are silent on the compliant edge (neither emitsguard.compliant).
systemd's richerActiveStatecollapses 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: enforcerule 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 asystemctl daemon-reexec/ dbus restart.
Build/runtime dependency on Linux agents:libsystemd— buildlibsystemd-dev
(systemd-develon RHEL/Fedora), runtimelibsystemd.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 shippedDockerfile.agent,
Dockerfile.agent.chisel, and the asan/tsan images install it and keep that default.
Pass-Dsystemd_guard=auto(use if present) ordisabledto 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),/metricspublishesyuzu_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_clippedmakes any capping visible rather than
silent. Devices without the key export ascohort="(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
/dexper-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_Hourlyedge 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
theyuzu_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), allGuaranteedState: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.signalwebhook/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
procperfwarehouse 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
NtQuerySystemInformationsnapshot 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_pctis 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); setprocperf_enabled=trueto opt
in, independent of the device-level sampler. Queryable viatar.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-existingtar.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"}
plusyuzu_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/dexper-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_HourlyTAR 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
andExecution:Execute(the panel says so honestly otherwise); each query
is audit-logged (dex.device.perf.query). Agents with--dex-disableship
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/dexCatalogue 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 — writesHKLM fDenyTSConnections,
toggles the Remote Desktop firewall rule group locale-independently via
INetFwPolicy2, and startsTermServiceon enable; per-step status columns
withoverall=okonly when every step succeeds) andstatus(reads back all
three gates and derivesrdp=on|off|unknown, reportingunknownrather than
offwhen a gate is unreadable). Bundledwindows.rdp.set_statedefinition
(role-gated,endpoint-admin) andwindows.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 localAdministratorsgroup (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 helperinstall-server-postgres.shnow 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_restoreprocedures for
native and Docker deployments indocs/user-manual/server-admin.md(new
"PostgreSQL Substrate" section) andupgrading.md, including the ADR-0010
restore-pairing invariant (database andKeyProviderkeys-dir backups
restore together; runbook tracked in #1341) and an explicit warning never
totara livepostgres-datavolume; (3) the native UAT rig
(scripts/start-UAT.sh) now stands up ayuzu-postgres:localsidecar
container on loopback:15433with per-run random, distinct
superuser/app-role credentials, exportsYUZU_POSTGRES_DSNto the server,
and tears the container down onstop— a missing docker/sidecar is a
warning, not a failure, until the #1320 PR 3 fail-closed boot lands
(PG_SOFT_FAILflag). 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
perfwarehouse 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_Hourlyavg/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_perfaction andconfigurekeysperf_enabled(default
true) andperf_interval_seconds(default 30;0disables the trigger
entirely). Queryable viatar.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 (severitywarn) and fires a new
dex.blast_radiuswebhook/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}_totalyuzu_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) emitsstorage.low(fixed volume ≥90%
full or <5 GiB free, 10-min cadence, viaGetDiskFreeSpaceExW) andhw.error
(battery full-charge capacity <80% of design, hourly, viaIOCTL_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.lowand 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 (
netprobeplugin). Active RTT, jitter, and packet-loss
measurement to operator-chosen targets using native system calls (no
shell-out):network.probe.icmp(ICMP echo —IcmpSendEchoon Windows,
unprivileged ICMP datagram sockets on POSIX; on Linux gated by
net.ipv4.ping_group_range, reportingnot-permittedrather than fake loss),
network.probe.tcp(connect-time RTT to a target:port, sub-millisecond,
unprivileged everywhere — use where ICMP is dropped), andnetwork.probe.dns
(getaddrinforesolution 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 driversaction and
device.hardware.driversdefinition list installed device drivers (name,
version, date, provider, class) viaWin32_PnPSignedDriveron Windows and
/proc/moduleson 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 kqueueDiagnosticReportsfolder-watch (.ips/.diag
crashes/hangs/panics/jetsam), asysctl(KERN_BOOTTIME)uptime scalar, an
incrementallog showunified-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-disablekill 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.pemmacOS silent install (Jamf / MDM)
sudo installer -pkg YuzuAgent-*.pkg -target /Linux install
# Debian/Ubuntu
sudo dpkg -i yuzu-agent_*.deb
sudo systemctl edit yuzu-agent # set --server address
# RHEL/Fedora
sudo rpm -i yuzu-agent-*.rpmPlatform archives
| Archive | Platform | Arch |
|---|---|---|
yuzu-linux-x64.tar.gz |
Linux (server + agent) | x86_64 |
yuzu-linux-arm64.tar.gz |
Linux (server + agent) | ARM64 (coming soon) |
yuzu-gateway-linux-x64.tar.gz |
Linux (gateway) | x86_64 |
yuzu-windows-x64.zip |
Windows (server + agent) | x86_64 |
yuzu-macos-arm64.tar.gz |
macOS (server + agent) | ARM64 (Apple Silicon) |
Compose Wizard
yuzu-compose-wizard-*.zip — browser-based step-by-step
generator for a customised docker-compose.yml + .env.
Zero dependencies — extract the zip and open index.html in
a browser. Contributed by
@fjarvis in
#405.
unzip yuzu-compose-wizard-*.zip
cd yuzu-compose-wizard-*
# Open index.html in your browser, or:
python3 -m http.server 8000Docker images
docker pull ghcr.io/Tr3kkR/yuzu-server:v0.13.0
docker pull ghcr.io/Tr3kkR/yuzu-gateway:v0.13.0Architecture: linux/amd64. Images are published to GHCR by this workflow as part of the release.
Verify checksums
sha256sum -c SHA256SUMSVerify signature (cosign)
cosign verify-blob --bundle SHA256SUMS.sigstore \
--certificate-identity-regexp '.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
SHA256SUMSSupply chain: SBOM, SLSA provenance, signed images
Every release ships CycloneDX + SPDX SBOMs per platform
(yuzu-*-{linux-x64,windows-x64,macos-arm64}.{cdx,spdx}.json,
yuzu-{server,gateway}-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.