Repository navigation
Yuzu v0.14.0
⚠️ Docker Compose users: copy/etc/yuzu/certsout of the server container before you next recreate it. In 0.14.0,deploy/docker/docker-compose.yml(the README quickstart),docker-compose.uat.yml,docker-compose.full-uat.yml,docker-compose.viz-uat.yml,docker-compose.demo.yml(with--keep), and Compose Wizard output in Plaintext mode, with "Persist generated certs" unticked, or with named volumes off and an external Postgres, keep the server's CA and secrets key-encryption key inside the container. Recreating the container deletes them: that covers an image upgrade,docker compose downthenup, andup --force-recreate. The server then refuses to start (kek_unresolvable), and anything encrypted with that key, MFA secrets included, cannot be recovered. Don't recreate the server container, and don't rundocker compose down -v, until you have followed the copy-out recipe indocs/user-manual/upgrading.md, section "Docker Compose: copy/etc/yuzu/certsout of the server container before you recreate it". The compose files are fixed onmainand in 0.14.1 (#5370). Not affected:docker-compose.reference.yml,docker-compose.reference-gateway.yml, and non-Docker installs.
The full 0.14.0 changelog is longer than a GitHub release can hold. The list below is cut short; the complete
## [0.14.0]section is inCHANGELOG.md.
Before you upgrade
- Windows agent: set update-signing options in the service's
Environmentregistry value, not its binary path. Every run of the agent installer, including an upgrade pushed by SCCM, Intune or GPO, rewrites theYuzuAgentservice's binary path and silently drops--update-trust-bundleand--update-require-signaturefrom it. OTA updates are then no longer signature-checked. Windows: the service'sEnvironmentvalue indocs/user-manual/server-admin.mdgives the commands and a check. (#5196) - Windows agent installer: pilot it first. It now refuses, with exit code 7 and the reason in its
/LOG=file, an%ProgramData%\Yuzu\agent-certsfolder it did not secure, or one holding anything other than plain files owned by Administrators or SYSTEM. If it refuses, the agent service is left running. If an endpoint ran a 0.14.0 release candidate from rc1 to rc5, see OTA update binary signing in the user manual to repairagent-certs. 0.13.0 endpoints are not affected. (#5196) - Linux native packages need a recent distribution. See the known issue below. The container images are the supported route on older hosts. (#5143)
Known issues
Windows server installer (#5196, #5210)
The Windows server installer adds permissions to %ProgramData%\Yuzu Server but does not remove the ones it inherits, so local users on that machine can read its contents. That includes yuzu-server.cfg (password hashes) and any TLS key copied into certs\. A silent install's /LOG= file also contains the installer's full command line, including any passwords passed to it: protect or delete it. Windows server is not a supported deployment (ADR-0035), and a service registered by this installer cannot start without a manually configured Postgres connection (#5272). If you run it anyway, restrict that folder to Administrators and SYSTEM, and do not let untrusted users sign in to the machine. A redesigned installer is planned for 0.14.1.
Linux native packages need a recent distribution (#5143, #5171)
The .deb, .rpm and tarball binaries need glibc 2.38. The server also needs GCC 15's libstdc++, and the agent GCC 14's. In practice the server runs on Ubuntu 26.04 and Fedora 42, and the agent also runs on Ubuntu 24.04. None of them runs on Ubuntu 22.04, Debian 12 or RHEL/Rocky 9: the server and agent .rpm refuse to install there, while the .deb packages install but do not start. The gateway also needs OpenSSL 3, and its .rpm does not start on Fedora 42. The container images are not affected (except that the Linux agent delivered by yuzu-agent-bundle-chisel has the same requirements); use them for the server and gateway elsewhere. Building to an older baseline is planned for 0.14.1.
Added
-
ADR-0031 operator surface: server wiring, MCP twins, and the authenticated
upload protocol replace the legacy file-retrieval stub (PR1.5c + PR1.6c).
PluginConfigStoreandUploadGrantStore(PR1.5b/PR1.6a) are now constructed
at server startup and wired to their REST routes
(/api/v1/plugin-config/*,/api/v1/upload-grants*) and to eight new MCP
tools (get/list/set/delete_plugin_config,set/delete_plugin_secret,
get/set_plugin_kill_switch), alongside the three upload-grant MCP twins
(mint/list/revoke_upload_grant) already shipped in #3135 — all eleven
carry the same securable, operation, and audit verb as their REST twin. The
agent-authenticated upload session endpoints (POST /api/v1/uploads,PUT .../chunk,GET .../{upload_id},POST .../commit,DELETE .../{upload_id}) are documented in the OpenAPI spec but deliberately have
no MCP twin — they authenticate on a grant/session bearer credential, never
an operator session. The legacy, unauthenticatedPOST /api/v1/file-retrievalstub (accepted a body-suppliedagent_id, stored
nothing) is removed; exactly one handler now serves file retrieval.
ScheduleRunner's arming re-check (PR1.5a) is wired to a real
authorization lookup: a schedule whose creator has since lost the required
permission for its targetplugin.actionno longer fires, on both the
auto and approval-gated paths.PluginConfigStore's secret column is now
enrolled in the live KEK rotation/rewrap/status surface alongside the auth
store's — an operator's/secrets/kek/rotateor/rewrapcall re-wraps
plugin secrets too, closing the gapdocs/adr/3005-plugin-config-store.md
recorded as open. (PR1.5c/PR1.6c) -
Plugin config/secret plane + per-action kill switch. Born-on-Postgres
PluginConfigStore
(schemaplugin_config_store) gives later default-off plugins a place to keep operator
configuration, envelope-encrypted secrets (SecretCodec, enrolled in the live KEK-rotation
surface), and a per-plugin or per-action kill switch. REST surface at
/api/v1/plugin-config/*: list/get/set/delete config (PluginConfig:Read/Write/Delete),
set/delete secret (PluginSecret:Write/Delete, write-only — no route ever returns a secret's
plaintext), get/set kill switch (PluginConfig:Read/Write). Kill-switch evaluation is
fail-closed: a degraded store reads as "disabled", never "enabled". Agent-side secret delivery
is a deliberately open item — seedocs/adr/3005-plugin-config-store.md. (PR1.5b) -
Typed schedule parameters + arming re-check on every fire path
(PR1.5a).POST /api/schedulesnow accepts an optionalparameters
object (string/number/boolean values only; nested objects/arrays, an
empty/underscore-prefixed key, more than 32 keys, or a canonical
serialization over 4KB are rejected with a400). The stored form is
canonical — keys sorted, so the same logical parameters always serialize
identically regardless of the order the caller wrote them in — and reaches
the scheduled dispatch exactly as a real
std::unordered_map<std::string,std::string>, replacing the hardcoded
empty"{}"ScheduleRunnerpreviously sent on every fire. Parameters are
set at creation only; there is no update route (delete and recreate to
change them). Existing schedules migrate forward with the canonical empty
object and are unaffected.
Separately,ScheduleRunner::fire()now re-verifies the arming principal's
authority (Deps::arming_check) BEFORE branching on approval, so the
re-check covers both the direct-dispatch (approval_mode=="auto") path
and the approval-gated path — not only the latter. The callback is
fail-closed: unset, it denies the fire, audits the denial (principal +
targetplugin.action), counts it, and still advances the schedule so a
permanently-denied schedule cannot spin. It sits in front of the existing
approval-ticket flow (ApprovalManager/fire_with_approval) as an
additional gate, not a replacement for it. -
One-time upload grant + authenticated chunked artifact receive (CC-06 fix, server side). The
server gains a new/api/v1/upload-grants(mint/list/revoke, operator-gated) +/api/v1/uploads
(session open, chunked PUT, status/resume, commit, cancel) REST surface backed by a new
born-PostgresUploadGrantStore, replacing the trust model of the old
POST /api/v1/file-retrievalstub — which accepted a client-assertedagent_idand destination
path with no real authentication and never durably wrote the uploaded bytes. An operator mints a
single-use grant naming the agent/source/size/hash bounds; redeeming it (atomically, exactly
once) opens an upload session whose own credential authenticates every chunk. Neither credential
is ever stored in plaintext (SHA-256 digest only, one-time reveal at mint/session-open). See
docs/adr/3004-artifact-blob-storage.mdfor the design. -
Cross-fragment capability-catalogue drift gate (PR1.9). Two new hermetic tests keep the five per-plugin-group
capability_decls/plugin_action_catalogue_*.hppfragments pluscore_dispatch_capabilities.hpphonest as a whole, since no single fragment's author can see the other five.tests/test_capability_catalogue_complete.py(python3 tests/test_capability_catalogue_complete.py) parses every plugin'sactions()override and cross-references it against all six catalogue sources, naming the exactplugin.actionwhen a plugin declares an action with no catalogue row, a fragment declares a row no plugin actually has, or the sameplugin.actionis declared by more than one source.tests/unit/server/test_capability_catalogue.cpp([server][dispatch][capability]) composes aCommandCapabilityRegistryover all six sources and asserts the catalogue-wide invariants: every row'srisk_tieris at or above its operation's floor, everysecurable/operationis onerbac_store.cppactually seeds, everyDestructiverow isIrreversible,system_reservedis true only forcore_dispatch_capabilities.hpprows,classify()resolves every declared pair, and a locally-constructed duplicate span makes the registry reportAmbiguous. -
Command capability classification core (PR1.9a). New header-only
server/core/src/command_capability.hpp/command_capability_parsers.hppdefine theplugin.actionclassification vocabulary (DispatchClass,Mutability) and aCommandCapabilityRegistrythat composes several independently-authored capability fragments and classifies a dispatch case-insensitively — an unknownplugin.actionis alwaysUnclassified, never a permissive default, and the sameplugin.actiondeclared by two fragments isAmbiguous, never first-wins. Also ships the frozenv1|<class>|<mutability>|<plan_hash_hex>dispatch-tag wire grammar (encode/decode) and an order-invariantcompute_plan_hash.capability_decls/core_dispatch_capabilities.hppdeclares the three system-initiated dispatches (tar.fleet_snapshot,__guard__.push_rules,asset_tags.sync). Not yet wired into a live dispatch path — a future PR consumes this registry from the dispatch chokepoint.authz_model.hppadditionally gains three newCapabilitySeedrows (PluginConfig,PluginSecret,UploadGrant), reusing only existingOperationenumerators.RbacStore::seed_defaults()seeds the same three securables intorbac.db— Administrator gets full CRUD via the existing loop, PlatformEngineer gets Read/Write/Delete on all three, and Operator gets Read onPluginConfig/UploadGrantonly, neverPluginSecret— closing the gap (peer finding PLAN-001) where a securable absent from the live seed cannot receive a default grant, denying even an Administrator on a fresh RBAC-enabled install. -
Dispatch classification tag on the wire, with a gateway wire-capability handshake (CC-03).
CommandRequestnow carriesdispatch_tag(field 9), the opaquev1|<class>|<mutability>|<plan_hash_hex>token the dispatch chokepoint uses to classify a command as read-only, mutating, or destructive. The field is mirrored into both of the Erlang gateway's vendoredagent.protocopies and forwarded untouched on the command-fanout path, the same way the existing Guardianpayloadfield is. Because gpb silently drops any field a gateway build's vendored proto doesn't declare, the gateway now advertises the literalcommand_dispatch_tag_v1inStreamStatusNotification.wire_capabilities(a new field on the same connect-time message the gateway already sends) so the server can tell a gateway that would strip the tag apart from one that carries it end to end. -
Weekly leaked test-database sweep (#1367). The
cache-prune.ymlmaintenance run now sweeps leakedyuzu_test_*databases off every persistent self-hosted CI Postgres instance (scripts/ci/sweep-test-databases.sh): epoch-named databases age by the same >6h server-clock rule as the in-suite sweeper, and names the epoch sweeper can never parse (pre-epoch format, implausible clock stamps) are reclaimed once the datdir'sPG_VERSIONmtime exceeds 7 days with zero active backends — closing the "idle box never sweeps" and "permanently unsweepable names" gaps. The same round fixed the Windows prune job's silent no-op (findresolved to System32's, not coreutils') and pinnedclean: falseon the new checkout steps so the weekly job can't wipe the runners' warm build trees. -
macOS crash reports now carry the application version.
.ipscrash and hang reports route the app'sCFBundleShortVersionString(from the report headerapp_version, falling back to the bodybundleInfo) through the sharedcanon_versionnormaliser into the DEX signal's version field, so macOS crashes join the per-(app, version)reliability identity the same way Windows crashes do. Unversioned binaries (unsigned CLI tools) report an empty version, exactly as store/packaged apps already do on Windows. -
macOS DEX unified-log collector now observes Fault-level events (#1372). The
logpredicate that pre-filters the firehose insidelogdnow requestsmessageType == "fault"alongsideerror, so Fault-level signals — notablycom.apple.apfsfilesystem corruption — reach the parser instead of being silently dropped before the agent ever sees them. -
tar.db corruption is now newly detected fleet-wide (#1567; see
docs/user-manual/tar.md"Fleet visibility" for known limitations —
audit-row latency during a mass event, and no backfill for a device
quarantined before this shipped).tar.statusgains a
db_health|<ok|quarantined>|<epoch>row; the tar plugin publishes
heartbeat.db_corruption_total/heartbeat.db_quarantine_last, which a new
generic plugin heartbeat-tag bridge forwards asyuzu.plugin.tar.*tags. The
server exposesyuzu_fleet_tar_db_corruption_agentsand
yuzu_fleet_plugin_init_failed{plugin}, writes atar.db.corruption_quarantined
audit row per (agent, quarantine identity), deduplicated against the audit store
and rate-limited per agent per server process, and a newyuzu.plugins_failed
heartbeat tag distinguishes a plugin that failed init from one that is not
installed. -
Per-OS DEX health score under the catalogue OS filter (#1746). Selecting a single-OS chip (Windows / Linux / macOS) on the
/dexsignal catalogue now scores each family against that platform's online agents and its own signals, instead of always showing a Windows-denominated number under any OS heading. A macOS- or Linux-only fleet now shows a real per-family score rather than a misleading Windows-derived one; the all-connected view is unchanged. -
TAR perf status tokens and per-source health (#1846). New
counters_unavailable
collect token for a supported platform whose counter reads failed.tar.statusgains
<source>_last_status,<source>_consecutive_failuresand<source>_last_status_at
per source, plusprocperf_procs_seenandperf_capture_method. -
SparkEngine detection engine — instantiated observe-only (ADR-0021 Stage-2, rung 1). The agent now stands up the next-generation event-driven detection engine (file / service / registry watches) alongside the existing Guardian path, running observe-only with no consumer — it reports its own health but does not yet drive detection or enforcement, so nothing about how Guards detect or remediate changes. New boot-time opt-out
--spark-disable/YUZU_AGENT_SPARK_DISABLE(the enforcing legacy path is unaffected). New os-labelled fleet metricsyuzu_fleet_spark_reporting,yuzu_fleet_spark_disabled,yuzu_fleet_spark_failed,yuzu_fleet_spark_mechanisms,yuzu_fleet_spark_armed_faulted,yuzu_fleet_spark_{watch_rejected,quarantined,slow_op}, andyuzu_fleet_spark_{watch_faults,queued_dropped,consumer_errors}. A reviewedyuzu-fleet-sparkPrometheus alert group ships commented out — every spark counter is provably 0 at rung 1, so an enabled rule could only fire on a forged heartbeat; it goes live at rung 2 (#2083).yuzu_fleet_spark_mechanismscounts only mechanisms that are registered and functional: one that could not bind its OS facility (e.g. a containerised Linux host, which has no systemd system bus) is reported inert rather than advertised as a capability it cannot honour. See the user manual's "SparkEngine — the next-generation detection engine" section. -
/dev-teamsenior-led delegation workflow is now committed project-level tooling. The
skill (.claude/skills/dev-team/) plus its two agents (junior-developer,
enterprise-architectin.claude/agents/) move from personal user-global config into the
repo, so every collaborator gets them via git instead of installing by hand. An Opus "senior"
session decomposes a task, dispatches Sonnetjunior-developersubagents in parallel,
autonomously resolves their escalations (consulting a Fableenterprise-architectfor
material or disputed calls), then integrates behind/test+/governance— committing the
reconciled tree before governing it so the gate always covers the exact range that gets
pushed. Additive only; no existing agent or skill is modified. Invoke/dev-team <task>(#1959). -
Per-principal quota cap for engine principals (#1973). Every
principal_kind=="engine"session is now capped at the server's single
pre-routing chokepoint on two independent dimensions — in-flight
concurrency (--principal-max-concurrency/YUZU_PRINCIPAL_MAX_CONCURRENCY,
default 16) and per-principal token-bucket rate
(--principal-rate-limit/YUZU_PRINCIPAL_RATE_LIMIT, default 20/s, burst
2x rate). Exhausting either cap returns HTTP429+Retry-After— the A4
error envelope on REST, a JSON-RPCid: nullerror (code-32010) on MCP
— and increments the pre-seeded, bounded-label
yuzu_server_principal_quota_exhausted_total{side,limit}counter (a
companionyuzu_server_principal_quota_admits_total{side}counts every
admitted request, so the exhaustion rate is computable); human,
device-agent, and anonymous traffic is unaffected. Both dimensions apply
to streaming/SSE requests too — a streaming request holds its concurrency
slot for the stream's lifetime rather than being rate-capped only. This
closes the ADR-1005 interlock requiring a minimum per-principal cap before
any engine principal may be enabled in production. The cap is
per-server-process (a multi-replica deployment's effective ceiling is
configured_cap x replica_count; durable cross-instance quota is a future
follow-up), and a rejection is metric-only with no audit row (a
high-frequency operational event, not a lifecycle action). See
docs/user-manual/engine-principals.md"Per-principal quota cap". -
HA: background-job replica-safety classification + the last #2508 clock guards (ADR-2002 WS-10,
slices 10.1/10.2). Every server background pass is now classified for replica-safety in a
checked-in, CI-auditable table (server/core/src/background_jobs.hpp) — ReplicaSafe /
FencedLeaderOnly / DisabledUntilFixed, one entry per pass (a single thread runs many), each with
its single-writer rationale; aconstevalsite-gate makes removing or renaming a classified
pass's table entry a build failure, and a table test pins the inventory's consistency + count
(proving every dispatch site is classified — full pass⇒named coverage — is a tracked follow-up).
Enabling a second replica needs this so a background job cannot silently double-run. Separately, the three remaining bare wall-clock retention deletes
(app_perf_fleet_store,preflight_run_store,deployment_run_store, #2508) and the
concurrency_claimsstale-claim reconciler are now clock-guarded and SINGLE-WRITER-safe — the
three prunes via one sharedpg::run_clock_guarded_prunehelper (the full seven-part guard + a
pg_try_advisory_xact_lock, reading Postgresnow()in-SQL), the reconciler via the same advisory
lock. No operator-visible behaviour change; this is HA-foundation hardening (see
docs/ha-background-jobs.md,docs/clock-guarded-retention.md). Fenced-leader-only enforcement
(gating those passes on the leader) lands with WS-3 3.2. -
HA WS-2a (ADR-2002 §5): a durable Postgres event outbox for execution
events.ExecutionTrackergains a third table (event_outbox, migration
v4) that durably records the transition events it already fans out in-memory
overExecutionEventBus—agent-transition,execution-progress, and
execution-completed— each carrying a global durable monotonicevent_id
(BIGINTIDENTITY). Every event is appended inside the same transaction as
the state mutation that produced it (theagent_exec_statusupsert, the
aggregate recompute/terminal transition, and the cancel), so a crash or
failure can never leave state without its event or vice versa; the two
formerly-autocommit write paths (upsert_agent_status_once,mark_cancelled)
are now transactional. The durable feed has no consumer in this slice — the
in-memory bus still serves live SSE unchanged — and is bounded by a
clock-guarded retention sweep (reap_event_outbox, 24h window) that mirrors
thecommand_executionretention shape. This is the foundation the fenced
leader (WS-3) and the cross-replica LISTEN/cursor-poll delivery (WS-2a-2)
build on. Server-tier HA remains gated on the full safe-to-scale set; a single
server is unaffected. -
HA: cross-replica execution-event delivery (ADR-2002 §5, WS-2a-2). The durable
event_outbox(WS-2a-1) is now drained cross-replica: each server replica runs a ~2s poll that
re-publishes execution-history events (agent-transition/execution-progress/
execution-completed) which originated on other replicas onto its own live SSE bus, so a
subscriber connected to one replica sees live execution progress driven from any replica. The
poll cursors on a Postgres commit-settle horizon (w_xid < pg_snapshot_xmin(pg_current_snapshot()))
so its live forward delivery never straddles an in-flight event, skips the replica's own
already-published rows, and is at-least-once (duplicates tolerated). New metric
yuzu_exec_outbox_poll_published_total. The live SSEid:remains the reconnect-safe per-channel
counter (unchanged) — the durable globalevent_outbox.event_idis kept in the outbox for the
cross-replica poll cursor and for the later durable failover-replay slice, deliberately NOT put on
the live bus (that would let a commit-order inversion strand a committed event from a cursor-based
subscriber). Single-replica deployments are unaffected (the poll finds nothing foreign to deliver).
Loss-free reconnect across replicas (durable outbox replay ordered byevent_id) is a WS-2a-2
follow-up and a precondition for enabling a second replica. -
HA: fenced leader-election primitive (ADR-2002 §3/§6/§10, WS-3 slice 3.1). Added a
Postgres-backedLeaderElector— the coordination primitive that will let exactly one core replica
run the singleton background loops (schedule tick, policy remediation, reconcilers) once a second
server replica is enabled. It owns a dedicated, never-recycled connection holding a session advisory
lock, and mints a strictly-monotonic epoch on each acquisition (recorded in a new
leader_elector.leader_statetable); a side-effecting claim fences on that epoch so a paused
ex-leader whose lock silently moved cannot commit. This slice ships the primitive and its schema
only — no background loop is wired to it yet, so there is no runtime behaviour change; gating the
loops (3.2) and the transactional command outbox (3.3/3.4) follow. Enabling a second replica remains
gated on the full WS-3 stack landing (see the HA delivery matrix). -
SCIM Groups → role mapping (#2021).
/scim/v2/Groupsnow accepts
POST/GET/PUT/PATCH/DELETE(list with pagination and
filter=displayName eq "..."), and--scim-admin-group
(YUZU_SCIM_ADMIN_GROUP) grantsrole=adminto any SCIM-provisioned user
currently a member of that group — mirroring the existing SAML/OIDC
group→role mapping (SOC 2 CC6.7). The Users-slice provenance guard is
preserved: only accounts SCIM itself provisioned are ever role-changed.
Note the deprovision-ordering interaction — a group-granted admin cannot
be SCIM-deprovisioned until the IdP first removes them from the admin
group (seedocs/user-manual/scim-provisioning.md"Groups → role
mapping"). -
yuzu_http_requests_total{principal_class="engine"}is now live (engine principals PR 4.5). Theprincipal_classlabel on HTTP request counts previously withheldengine— a resolved engine-principal request was misclassified asagent(indistinguishable from a human API token at the header level). The metric now piggybacks the session already resolved at the pre-routing chokepoint for the PR 4.4 quota gate (zero extra per-request auth cost):engineis emitted for a successfully RESOLVED engine-principal session, whilehuman/agent/noneremain presentation-based fallbacks — a request bearing an engine token whose session resolution fails still classifies asagent, neverengine. The closed-set pre-seed at startup now covers all four values. Seedocs/observability-conventions.mdfor the hybrid-basis contract. -
RHEL 9 / Rocky 9 / AlmaLinux 9 build support.
scripts/setup-rhel9.shprovisions the full C++
toolchain (gcc-toolset-14 for C++23, CRB-sourced ninja, the vcpkg port prerequisites, pip-pinned
meson + PyYAML, vcpkg at the pinned baseline, optional ccache from EPEL, and optionally a local
PostgreSQL 18 for the server test suite). The script is idempotent, has a--checkverify-only
mode for confirming parity on a second machine, and can emit a provenance manifest (--manifest). Runbook:
docs/rhel9-build-setup.md, which documents the four RHEL-specific traps — the system GCC 11
cannot build C++23; onlymeson/native/linux-gcc13.iniis usable (the gcc14 file hard-codes
gcc-14/g++-14binary names that Software Collections do not provide, and the gcc15/clang21
files require mold);postgresql-setup --initdbleaves host auth onident; and the test role
needspg_signal_backendon top ofCREATEDB, without whichPostgresTestDb's
DROP DATABASE ... WITH (FORCE)silently leaks databases until the[pg]shard blows its meson
timeout. Also records a platform limitation: RHEL 9's system SQLite (3.34.1) predates
RETURNING(3.35+), so the Python test-telemetry tooling cannot run there; the C++ suites are
unaffected because they link vcpkg's SQLite 3.52.0. -
GET /api/v1/network/fleetand the MCPget_network_fleettool now returnavailable_keys,
the fleet's tag keys for the cohort-picker UI — previously only the/networkdashboard fragment
had this, so an API/agentic caller could not populate the same cohort picker the dashboard shows.
Closes that dashboard/API parity gap. (DEX exposes the equivalent via its dedicated
GET /api/v1/dex/perf/cohortsroute; network has no cohorts route, so the distinct tag keys are
surfaced on the fleet resource itself, bounded by the 5s network snapshot cache.) -
DEX Apps tab cross-links to per-version fleet performance, with a version filter.
The Apps tab's crash/hang blast-radius page now links to that application's fleet-wide
CPU & memory trend (and back), joined on the exact process-image key crash and perf
identity already share — no fuzzy or display-name matching. The performance trend page
gains a per-version filter (click through a version row to narrow the view, or back to
"all versions"), using the sameversionparameter the REST API already accepted. -
DEX App Performance: discoverable navigation, kernel-thread filtering, per-version device drill-down, and a device-model cohort filter. The per-app performance trend is now a top-level App Performance tab (previously a small inline link on the Performance page) with search, platform, and sort controls over the application picker. Server-side rollup now excludes kernel threads from per-app CPU/memory samples (
PF_KTHREAD), so a device's application list reflects real userspace processes only. Each version row on the trend can be expanded in place to list the devices behind that number (fleet-wide only, top-N sample, 31-day retention, audit-loggeddex.app_perf.devices.view). A new device-model cohort filter narrows the trend to devices carrying one tag value (e.g. one hardware model) — mutually exclusive with the existing management-group scope, same statistical floor, and shipped with full REST (GET /api/v1/dex/perf/tag) and MCP (get_dex_tag_app_perf) twins alongside the dashboard control. -
Service Spark watches now report a positive establishment signal. Arming a
systemd-unit or Windows-service watch previously told the caller only that the
request was accepted, not that live OS-level notification coverage (a real
PropertiesChangedmatch on Linux, a realNotifyServiceStatusChangeW
registration on Windows) actually exists yet.SparkEngine::subscription_establishment(id)
is a new pull query returning when the subscription was armed, whether it has
ever been confirmed established, and its current tri-state coverage
(none / notification / poll-fallback) — a third, independent fact alongside the
existing accepted/resolved timestamps, not a redefinition of either. Additive:
every other mechanism (Registry, File) and every existing consumer is
unaffected. Registry/File get the same signal in a tracked follow-up (#4340). -
RBAC enforcement enable/disable toggle (A1). RBAC enforcement can now be switched
on or off fleet-wide viaPUT /api/v1/rbac/enforcementand the MCP twin
set_rbac_enforcement— durable, converging across every replica within ~1s. There is
no config-file key and no Settings-page control for this (neither ever existed,
#388). On a real transition, the toggle
refuses (403) unless the CALLING operator holds authority under the regime that is
durably true right now (closing a cross-replica cache-staleness gap in the admin gate
itself), and refuses (409) unless they would remain a durable administrator under the
destination regime: enabling requires the caller's own fleet-wideAdministrator
grant, disabling requires the caller's own local account to hold theadminrole —
each refusal names the exact follow-up call to make. The guard runs inside the same
transaction as the flag write, under therbac_metarow lock; the enable direction's
destination check additionally takes the identicalFOR UPDATE OF prlock the existing
last-Administrator guard takes, so the two serialize against each other rather than
racing (every other check in the guard is a lock-free read). Idempotent (requesting the
current state is a no-op, evaluating neither check) and audited
(rbac.enforcement_changed), with a newyuzu_server_rbac_enforcement_enabledgauge, a
yuzu_server_rbac_enforcement_toggle_totaloutcome counter, and both a threshold-free
YuzuRbacEnforcementChangedalert and a direction-awareYuzuRbacEnforcementDisabled
warning for the security-regression direction. A bearer token minted with a restrictive
mcp_tier(e.g.readonly) whose principal happens to be a durable administrator can no
longer reach this route via REST with no MFA/approval gate — REST now refuses any
non-emptymcp_tieroutright, the same posture every other admin-only REST route
already had. This same fresh-regime-read fix also hardens the existing fleet-wide
role-assignment routes (POST/DELETE /api/v1/rbac/roles/{name}/assignmentsand MCP
twinsassign_rbac_role/unassign_rbac_role): both now re-verify, with a fresh,
uncached read, that the caller holds authority under the regime that is durably true
right now, closing the identical cross-replica cache-staleness gap in their own
durable-Administrator check — a grant minted through a stale-regime read would
otherwise persist indefinitely once written, unlike this toggle's own transient
window. -
/dexCatalogue family cards get a public REST + MCP resource.GET /api/v1/dex/catalogueand the MCP twinget_dex_cataloguenow serve the
per-family monitored/total type count, health-score slice, window event
count, and busiest member type the/dexCatalogue grid has always
computed, plus anuncatalogued[]list of signal types seen on the wire but
not yet in a curated family — the first public resource for this data (no
per-agent identity; not audited). A degraded fleet signal-summary read now
answers503(retryable) rather than a fabricated healthy, zero-event
catalogue.GET /api/v1/dex/overviewand its MCP twinget_dex_overview
additionally carryconnected_platforms,busiest_family, and
busiest_family_events, mirroring the fields the Overview hub's own
coverage tile and Explore card already show.GET /api/v1/dex/catalogue/groupand its MCP twinget_dex_catalogue_group
additionally carrybenign, true for the one family whose window activity
is routine reports rather than incidents (the same flag the/dex
Catalogue drill's "Reports (window)" vs "Events (window)" label already
keys off). -
macOS
/networkthroughput. The agent now reports device network throughput on macOS: rx+tx byte rates summed across non-loopback interfaces via the routing-socket interface list (sysctl NET_RT_IFLIST2), differenced across heartbeats. Previously the macOS sampler returned an all-invalid sample and shipped no network tags; it now populates the/networkthroughput card alongside Windows and Linux. Retransmit and RTT remain deferred on macOS: the system-wide TCP retransmit counters (net.inet.tcp.stats) are unmaintained on current macOS, so retransmit awaits a per-connection source; heartbeat RTT is likewise deferred, as in the Windows heartbeat sampler (Windows' separate opt-in per-connection ESTATS tier is unchanged). -
Envelope-encrypted secrets now emit audit events and a Prometheus metric on decrypt failure.
yuzu_server_secret_decrypt_failures_total{store,failure_class}counts failures by class
(tamper-detected, unresolvable KEK, malformed blob), and the KEK lifecycle verbs
(kek.generated,kek.rotated,kek.retired) plussecret.decrypt_failureare written to the
audit log. Required by ADR-0010 for the first store to hold envelope-encrypted secrets at rest
(the auth store's TOTP secrets); without it a fleet could fail every MFA decrypt with no signal. -
registry.list_profilesenumerates local Windows user profiles. ReadsHKLM\...\ProfileListand reports each profile's SID, resolved name, profile path, and whether its registry hive is currently loaded underHKEY_USERS— the discovery step other per-user registry actions need, previously unavailable as its own action. System profiles (LocalSystem, LocalService, NetworkService) are excluded. -
antivirus.av_exclusionsaction. Reads Windows Defender's exclusion lists (HKLM\SOFTWARE\Microsoft\Windows Defender\Exclusions\{Paths,Processes,Extensions}) directly from the registry and reports each excluded path, process, and extension. Windows-only; a key ACL'd against the reading account reports a typedpermission_deniedrow rather than a silent empty list. -
firewallplugin's Linux backend probe gains a real nftables leg.state/rulesnow read nftables directly overNETLINK_NETFILTER(rung 1, bounded — no libnftnl/libmnl dependency) ahead of the ufw/iptables argv fallback, replacing the stub that always fell through. Read-only table/chain/rule enumeration (base-chain hook + policy, per-rule handle); mutating nftables rules is out of scope (ADR-3002 Decision 8). -
Plugin README standard (
docs/plugin-readme-standard.md). Agent plugins document themselves beside their code inagents/plugins/<name>/README.md: eight fixed sections covering what the plugin does, how it works, per-OS capability, privileges, the data contract, real sample output per OS, caveats and source paths.tools/plugin-doc-genregenerates the generated parts (and the user-manual plugin index, the docs-site navigation and acontent/plugin-docs/<name>.jsonmanifest) from the committed sources with no build;tools/plugin-capturewrites stamped samples through the agent's real dispatcher and plugin lifecycle, and the mesondocssuite plus a docs-lint touch rule keep both honest. The manifests are embedded in the server and served asGET /api/v1/discover/plugin-docsand the MCP resourceyuzu://plugin-docs, with a per-plugindocssummary joined intodiscover_plugins(catalog version 2 → 3); the definition DSL gains four optional column keys. Two pilots ship now (disk_actions,firewall); the other 49 plugins follow in the retrospective sweep, held by a README-existence ratchet that may only fall. -
Spark async-arm acknowledgment design (rung 9c). Added the design record for decoupling
GuardianEngine::apply_rules()'s Spark rearm window from OS-watch confirmation (docs/spark-stage2-guardian-consumer-design.md§R5), its intentional legacy-vs-spark delta row (docs/spark-legacy-delta-registry.mdA3), and its flip-gate ladder slot (docs/spark-flip-gate.md§3a). Reviewed across seven rounds (six external design-review rounds against the underlying plan, one adversarial review of this transcription) plus a full/governancepass.prefer_spark_-gated and inert in production today; no behaviour change. PR-1 through PR-6 (the implementing code) are separate, not-yet-started PRs. -
New read-only
autorunsagent plugin —listandcatalog. Enumerates persistence sources ("what starts automatically") across a 34-source catalog: Windows Run/RunOnce/RunOnceEx (HKLM and every reachable per-user HKU hive, live-first with an offlineRegLoadKeyWfallback underSeBackup/SeRestore), StartupApproved, Winlogon Shell/Userinit, AppInit_DLLs, Image File Execution Options, Startup folders, Scheduled Tasks (ITaskService COM, XML parsed via libxml2), and WMI permanent event subscriptions; Linux cron/anacron/at, systemd system and user timer units (with a single rung-2systemctl list-timersfallback used only when systemd is present but every unit directory is unreadable), XDG autostart,/etc/rc.local, and/etc/init.d(listing only); macOS launchd plists (system and per-user LaunchDaemons/LaunchAgents), Login Items (constrained — no public read API),/etc/periodic, and/etc/emond.d.catalogperforms no OS call at all — a pure reflection of the plugin's own versioned source catalog and which support level this build declares for each source.listruns every leg and reports one status line per source plus zero or more data rows for sources that produced something, so a consumer always sees an explicit status for every source rather than inferring "nothing checked" from silence: a genuine acquisition failure (permission denied, a directory-walk cap or read error, a malformed crontab/task-XML entry) is always reported asconstrainedwith a named reason, never folded into "absent" or a falsedisabled/enabled; a per-sourceconstrainedoutcome on any of the three platforms additionally surfaces through the plugin's typed CC-07 result status, so a fleet-scale consumer reading only that field still learns of a real degradation. Root-scheduled Linux files (cron.hourly/daily/weekly/monthly,/etc/rc.local) are checked against their own owner/group/other execute bits, not the agent's own execute access, since they run under root's scheduler regardless of the agent's identity. Every acquisition is a direct file/registry/COM/WMI read except the one declared Linux fallback; zero process spawns anywhere on Windows or macOS.Securitysecurable, read-only, no approval gate. -
Known limitation: a per-user Windows Startup-folder redirect is read from the registry (
User Shell Folders\Startup), but%USERPROFILE%-style tokens in that value are expanded against the agent process's own environment rather than the enumerated profile's own path — and the agent runs as LocalSystem. That value is populated by Windows on essentially every profile at creation, not only genuinely redirected ones, so this affects the common case, not a narrow edge case. In practice this usually resolves to a path that doesn't exist on disk, so the affected profile's real Startup entries typically go completely unreported rather than a wrong-but-visible location being reported. This source is reportedconstrained(reasonstartup_redirect_env_mismatch) whenever the buggy path is taken, so this is never silently presented as a clean, complete result. Tracked in #4219; the underlying resolution is not corrected in this change. -
Sample-output provenance note:
agents/plugins/autoruns/docs/samples/linux.txtwas re-captured on 2026-09-10 against this shipped build (its predecessor predated the typed CC-07 aggregate status landing and no longer reflected it).macos.txt's row data is still the genuine 2026-09-08 capture; only itslistaction's[result_status]trailing line was hand-corrected to the value this build's code and tests prove it now produces (anyconstrainedrow aggregates toCONSTRAINED/PARTIAL/autoruns:degraded) — a real macOS re-capture is tracked in #4220, not yet done. -
Guardian arm-ledger and executor-ceiling fleet telemetry (rung 9c PR-3).
New heartbeat tagsyuzu.guardian_arm_pending/yuzu.guardian_arm_failed
(re-statable gauges of the ack ledger's current accepted-and-outstanding and
currently-failed spark arm counts) andyuzu.guardian_io_arm_disarm_rejected_ceiling
(a monitor-only counter of R5.1's physical-orphan admission ceiling), each rolled
up fleet-wide asyuzu_fleet_guardian_arm_pending/yuzu_fleet_guardian_arm_failed
/yuzu_fleet_guardian_io_arm_disarm_rejected_ceiling. Dormant whileprefer_spark
is off (every released agent today). -
New read-only
app_usageagent plugin —summary,last_used,foreground. Machine-scope application-usage evidence derived from TAR's default-onusagefold (usage_daily/usage_daily_user/usage_liveinsidetar.db, read-only,PRAGMA query_only):summaryreturns one row per executable over a window (days1-365,top1-500,byrun_time|run_count) — run count, total run seconds, first/last seen, a distinct-user COUNT (never a user name), superseded/expired runs — preceded by ameta|row carrying the fold's own health counters;last_usedreturns per-executable first/last-seen within TAR's retained usage window (31 days by default) plus a trailing-30-day window, optionally narrowed byexe. Executable names are pipe-escaped on the wire. No pid, command line or user name is ever emitted (a negative test pins this).foreground(per-session focus time) is alwaysconstrainedon every OS — session attribution is the user-context-bridge programme's, not this plugin's. A disabledusagesource, an older TAR schema, or an unreadable or corruptedtar.dbreportconstrained|<reason>, never an empty result; a stalled TAR usage/process collector additionally gateslast_usedthe same way (summaryinstead flags it via itsmeta|row'sfeeder_enabledfield).last_used's unfiltered form is capped at 5000 executables, reporting a trailingconstrained|last_used_truncatedrow past the cap;summary's distinct-user counts are bounded separately, to exactly the (up to 500) exe_keys already selected into its own ranked result set. Windows, Linux and macOS (macOS constrained: inherits the process source's ES-or-poll granularity).Forensicssecurable,AdminOrApproval, single-target, audited. A new ADR-0016app_usagedaily-sync source ships thelast_usedprojection to the server once a day (skipping, never sending an empty blob, when the source is constrained); it is registered as a typed source so a gateway-proxied report is never stored in the generic inventory. -
App-usage read surface (server). New
GET /api/v1/forensics/agents/{agent_id}/app-usageand its MCP twinget_agent_app_usagereturn one device's per-executable usage projection —exe_key,first_seen/last_seenwithin TAR's retained usage window (31 days by default), and a trailing-30-dayrun_count_30d/total_seconds_30d— ingested from the agent's daily sync (app_usagesource, ADR-0016; hash-skip over the raw received blob bytes,software_licensingprecedent) once the agent-side producer ships. Server half only in this change — the agent-sideapp_usageSyncSourcelands in a follow-up PR; until then both surfaces answer an honest empty projection ({"apps": [], "collected_at": 0}) for every agent, the same DORMANT-until-producer posture this repo already uses forVulnFindingStore(docs/postgres-migration-ladder.md). No user names, pids or command lines: the store carries none. Both surfaces requireForensics:Readscoped to the device (tier + management group, ancestor-aware) — and, on an installation with RBAC off, an Administrator session (Forensics:Readjoins the topology floor, matching the securable's documented Administrator-only posture) — emit a per-openapp_usage.agent.viewaudit and FAIL CLOSED (503,Sec-Audit-Failed: true) when that audit cannot persist; a store degrade is 503, never an empty 200. The newapp_usage_storePostgres schema joins the decommission cascade (DELETE /api/v1/sle/agents/{id}, now six stores) and theyuzu_inventory_stale_agents{source="app_usage"}freshness gauge; an ingest failure nacks the source so the agent resends in full next cycle, and typed-source rows are purged from the generic inventory store at boot. -
New read-only
execution_artifactsagent plugin —shimcache,amcache,prefetch(Windows only). Reads the three execution-evidence artefacts every Windows IR playbook pulls first: the AppCompatCache (ShimCache) registry blob (Windows 10/1110tslayout, path + last-modified per entry),Amcache.hveInventoryApplicationFile(path, SHA-1, size, link date, publisher, binary type — the live hive is copied then loaded privately viaRegLoadAppKeyW, serialised by the shared offline-hive lock), andC:\Windows\Prefetch\*.pf(MAM/XPRESS-Huffman decompressed in-process via ntdllRtlDecompressBufferEx; header versions 23/26/30/31, exe name, hash, run count, last eight run times). Paths and hashes only — never file contents. Linux and macOS reportunsupported|windows_only_artefact. Every action is on theForensicssecurable,AdminOrApproval, single-target (a fleet or scoped target is refused), audited, and the plugin ships default-off via the server-side plugin-config kill switch (seeded at boot, never clobbering an operator's own decision) — enable withPUT /api/v1/plugin-config/execution_artifacts/kill-switch. A truncated, malformed, unknown-version or over-cap artefact is reportedconstrainedwith a named reason, never an empty success and never an exception across the plugin ABI. -
New Hardware CI dashboard (
/hardware) — a ServiceNow-style Configuration Item list and record. A searchable, sortable, paginated device-CI list (KPI strip, OS/status filters) replaces the old Inventory Devices tab, and each device's record gains Overview, Installed software, Tags, DEX, Guardian, Live, and Actions tabs. The new Actions tab lists every action a connected agent's plugins expose, classified by capability (read-only/mutating/destructive) with a generated parameter form and a live dispatch-and-result panel — dispatch runs through the existing command route, so authorization, the destructive gate, and audit apply unchanged.GET /api/v1/hardwareandGET /api/v1/hardware/{id}are the REST twins. -
The standalone Devices page is now part of Hardware.
/devicesand/device?id=redirect to/hardwareand/hardware/ci?id=; the Hardware list gains IP, DEX score, agent version, and a Tags column with click-to-filter chips, and the CI record gains DEX, Guardian, and Live tabs (the same live-info cards the old device page showed). A new checkbox column and sticky action bar let you tag or untag a batch of selected devices in one action instead of one at a time. Breaking for custom roles — the list now gates onInventory:Readrather than the old page'sInfrastructure:Read; every seeded role holding one already holds the other, but a custom role granted onlyInfrastructure:Readloses list access on upgrade (see the Upgrading guide's "Hardware CI list requiresInventory:Read" entry). -
Physical-hardware detail on the device Live-info view. The Get live info card grid — reachable from a device's page and from the Hardware CI record's own Live lens — gains ten new cards: Disks, Memory, Processors, Drivers, Battery, Thermal, Disk health (SMART), Volumes, Network adapters, and Wi-Fi. Drivers reports on Windows and Linux; Disk health (SMART) and Volumes report on Windows and macOS; the other seven report on all three platforms.
-
Hardware CI record: sync-on-demand, tag editing, and a richer Actions tab. A Sync now button dispatches an immediate sync for one device (a single source or all of them) instead of waiting for the ~24h daily cadence, polling the record until the fresher data lands; an agent older than 0.13.1 is told to upgrade rather than silently ignored. The Tags lens gains inline add/remove. The Actions tab now shows each action's parameter syntax (drawn from the plugin's own docs) and a dispatch-result panel with free-text/regex search, a hit counter, and CSV export.
-
Reflex design contract, ADR-0021 amendment, and wire schema.
docs/reflex-design.mdis now
the canonical, implementation-citable contract for the Reflex agent-local automated-response
system (Spark-bound Reaction chains, YAML-authoritative content, digest-bound two-person approval,
thedevice_classconsent gate).proto/yuzu/reflex/v1/reflex.protodefines the wire schema for
Reflex Set deploy/status;GuaranteedStateEventgains an additivefamilyfield so Reflex
outcomes can reuse the existing Guardian event channel. No runtime behavior changes — this PR is
documentation and schema only; nothing is wired up yet. -
The server can now dispatch commands to the correct gateway when agents are split across
multiple independently-clustered gateways. Deployments running more than one gateway cluster
(for example, one per region) previously had every command forwarded to a single configured
gateway address, regardless of which cluster the target agent was actually connected to — a
command destined for an agent on a different cluster was silently misdirected. A new
--gateway-cluster-addroption (repeatable,cluster_id=host:port) lets an operator configure a
per-cluster gateway management address; the server now dials the cluster an agent's gateway
actually announced, falling back to the existing single--gateway-command-addrfor any
deployment that doesn't configure per-cluster addresses (no change in behavior for existing
single-cluster installs). A malformed or duplicate--gateway-cluster-addrentry fails startup
with a clear error. This is per-cluster routing only — it does not provide trust-zone isolation
between clusters for a given agent's identity, so do not rely on it to keep, e.g., a DMZ-zone
gateway from being able to intercept an internal-zone agent's traffic; that guarantee is tracked
separately and does not exist yet. -
Cross-replica agent presence (HA WS-5 slice 1).
AgentRegistrynow merges a durable Postgres-backed presence signal (OfflineEndpointStore, extended with a session-guarded liveness row) intoall_ids()/scope evaluation, so a scope-tagged dispatch on one server replica can reach an agent connected to a different replica once a second replica exists. Also fixes a latent dispatch bug where a presence-only agent id could reach the Broadcast fast path with no routing-directory consult and silently land innot_sent. Inert on today's single-replica deployment. -
Periodic Access Review export/campaign now states whether RBAC was actually enforced.
GET /api/v1/access-reviews/export, its MCP twinexport_access_review,GET /api/v1/access-reviews/GET /api/v1/access-reviews/{id}(and their MCP twins), and the frozen campaign rowPOST /api/v1/access-reviewspersists all carry a newrbac_enforcementfield —enabled|disabled|degraded— so the SOC 2 CC6.2 grant-population evidence no longer certifies a fleet-wide grant listing with no stated relationship to whether it actually governed access (RBAC ships disabled by default). The frozen campaign row stamps the value once, at open time, alongside the grant population it freezes. Seedocs/user-manual/rest-api.md'srbac_enforcementsection for the exact three-value semantics anddocs/user-manual/rbac.mdfor the staleness/degrade-during-partition caveats. -
Fresh-install RBAC bootstrap. A brand-new Postgres database's first boot now atomically
grants the config-file admin a durable, fleet-wide Administrator role alongside seeding the
account itself (RbacStore::provision_first_admin), closing a gap where RBAC's own
enable/assignment routes had no way to authorize themselves on a database that had never run
before. The event is recorded as a durable audit row (rbac.bootstrap.first_admin). -
Fleet-wide RBAC role-assignment listing.
GET /api/v1/rbac/roles/assignmentsand its
MCP twinlist_rbac_role_assignmentsreturn every(principal_type, principal_id, role_name)
grant row on record, across all three principal types, in one bulk read — the complete grant
table, not a management-group-confined slice. Gated on the same dedicatedAccessReview:Read
securable the SOC 2 access-review export uses, admitting the seededReviewerrole as well as
Administrator. -
Use-case-engine (UCE) host v1 requirements doc (
docs/uce-host-requirements.md, ADR-1005 exec-plan item 2c): captures plan Decisions 2–6/11–12/14 as tracked F/NF requirements, commits the Decision-14 findings-viewing confinement mechanism to view-time scoped read-through (server stays the confinement authority, zero staleness, per-view operator identity in Yuzu's audit) with a permitted interim mode until the ADR-0017 chokepoint lands, adds a host-universal operator-login/session security contract (NF-9 — Yuzu-as-identity-provider, no UCE user store, SSO inherited transitively, bounded-cache sessions), extends the scale target to ≥500,000 endpoints with no architectural ceiling, records the 2026-07-12 deployment topology (separate UCE database on the shared PostgreSQL instance, cross-DB access forbidden; UCE backend+GUI on its own VM — full design indocs/uce-deployment-topology-design.md), and registers the vuln module's findings store in the SOC 2 data inventory (enterprise-readiness-soc2-first-customer.md§3.5) with classification, a 90-day resolved-finding retention default, and decommission/DSAR/legal-hold deletion paths wired at the store-ship milestone. -
Suite-duration budget watchdog in CI test steps (#2093). Any meson suite exceeding 80% of its timeout budget now draws a
::warningwith measured/budget seconds (e.g.server unit tests at 512/600s (85%)), and every run — green included — appends a per-suite duration/budget table to the job summary, so duration creep toward a fixed budget degrades into a visible signal instead of a discontinuous wall of TIMEOUT failures. Reporting only; pass/fail semantics are unchanged and the watchdog is exception-guarded so it can never turn a green run red. -
Closes #Nnow works on dev merges (ADR-3001 pillar 2, #2139). A new
close-linked-issuesworkflow runs on every push todev, parses the merged PRs' bodies with
the repo's single closing-keyword grammar (scripts/tracker/closing_refs.py— GitHub's keywords
plus comma/andchains, code-fence/quote stripping, and negation suppression, acceptance-tested
against GitHub's ownclosingIssuesReferencesoracle with zero false negatives), and closes the
claimed issues ascompletedwith a self-identifying evidence comment naming the PR, merge SHA,
merging human, and run URL. Never-close rules are structural:security-labelled,
do-not-close-labelled,scripts/tracker/do-not-close.txt-listed, assigned, or open-PR-linked
issues get an advisory comment instead — and the driver fails closed if the never-close list is
missing. A per-PR cap (>6 refs) skips the batch into aneeds-triageissue rather than closing
in bulk. Liveness is three mechanisms: anif: failure()alert job (opens/updates an
automation-brokenissue, self-healing on green), a per-push leak scan, and a frozen parser
fixture corpus in CI. The bundled driver also carries the one-time #2139 backfill
(maintainer-reviewed dry-run diff pinned by a plan snapshot — execution aborts if any PR body
changed since review; security-labelled candidates are excluded from all mutation and presented
for review, with a zero-mutation hard-stop if one ever reaches a mutating action;--execute
requires a verified approval-comment URL on #2139) and--undo-pushfor exact-batch reversal.
A deterministic zizmor guard fails any PR that deletes the workflow, adds apaths:filter,
moves it off thepush:trigger, or adds any PR-context/dispatch/cron trigger. -
Issue-lifecycle standard adopted (ADR-3001, amended A1). Filing, labelling, and closing
GitHub issues now followdocs/agents/issue-standard.md: mandatory duplicate search before
filing, four body sections (Context / Evidence / Acceptance criteria / Origin), a three-axis
label contract (type + priority-or-roadmap+ triage state), and never-close rules for
automation —security-labelled,do-not-close-labelled, or listed in
scripts/tracker/do-not-close.txt(seeded with the deliberately-held-open security surface,
live before any close automation exists). ADR-3001 is accepted and amended in place (A1):
close-on-merge moves to apush-to-devtrigger, the triage sweep loses its autonomous
closure tier, the per-PR close cap drops to 6, and the CODEOWNERS artefact is descoped. The
/governanceand/testskills now file deferred findings per the standard (dedupe-first,
governance-deferredlabel, filed-and-not-filed run reports), and every instruction surface
(CLAUDE.md, AGENTS.md, CODEX.md, CONTRIBUTING.md, the PR template) routes to it. -
Execution child-query REST v1 + MCP twins,
get_execution_statusfield parity, and
list_schedules/GET /api/v1/schedulesfilters (api-parity programme, #2146 A2-R1). New
routeGET /api/v1/executions/{id}/childrenand new MCP toolget_execution_childrentwin the
legacyGET /api/executions/{id}/children- sameExecution:Readfleet-read gate, same #3789
confinement rule (a visible parent does not by itself disclose a child dispatched by, or
targeting, someone else; each child is checked independently, one batched per-child status
lookup rather than N+1), and a new sharedexecution_child_row_jsonbuilder
(execution_model.{hpp,cpp}) all three surfaces - REST v1, the legacy route, and MCP - now call,
so the row shape cannot drift between them.get_execution_status's output gains
parameter_values(redacted to"(redacted - confined view)"for a confined caller, exactly
likescope_expressionalready was) pluscompleted_at/parent_id/rerun_of, which stay
truthful for every caller - closing a field-parity gap against the REST v1 detail route
(GET /api/v1/executions/{id}), which already returned all four.GET /api/v1/schedulesand
MCPlist_schedulesgain optionaldefinition_id/enabled_onlyfilters.enabled_onlyis a
real boolean on both (gov docs-writer/cpp-expert fix round: the initial parse reproduced the
legacyGET /api/schedulesroute's presence-only quirk, where any presence of the param -
regardless of value - is treated as true; fixed to follow the #4034 precedent already set on
GET /api/v1/policiesbefore this PR shipped, matching MCPlist_schedules's own value-respecting
parse, which was correct from the start). -
REST v1 twins for the command/instruction-ID-keyed Responses API (#2146 A2-R2).
GET /api/v1/responses/{id},GET /api/v1/responses/{id}/aggregate, andGET /api/v1/responses/{id}/exportbring the legacy, unversionedGET /api/responses/{id}*family
to REST+MCP parity - sameResponse:Readfleet-read confinement (resolve-then-scope,
ADR-0017 INV-3) and query semantics as the legacy routes (unmodified, frozen reference code for
this PR). Distinct from the pre-existing, execution-ID-keyedGET /api/v1/executions/{id}/responses.
GET /api/v1/responses/{id}and MCPquery_responsesshare one JSON row builder
(response_query_row_json);GET /api/v1/responses/{id}/aggregateand MCPaggregate_responses
share another (response_aggregate_row_json) - both new, inresponse_query_model.{hpp,cpp},
perdocs/api-twin-recipe.mdRule 1. -
query_responsesrows now carryid,instruction_id,error_detail,plugin, and
received_at_ms, alongside the pre-existingagent_id/execution_id/status/output/
timestamp- all were already present onStoredResponsebut never surfaced to an MCP caller. -
aggregate_responsesgained anop_columnparameter (timestamp/status/id, default
id), validated againstResponseStore::allowed_op_column(). Previouslyop_columnwas never
read from the tool's arguments at all, so asum/avg/min/maxaggregate silently operated
on the store's own default operand column regardless of what a caller requested. A
present-but-wrong-JSON-typeop_column(e.g. a number) is rejected withkInvalidParams
rather than silently read as absent and defaulted toid(same defect class as #2970B/#2146
A2-R1'sparam_int_strict/param_bool_strict/param_string_strictfamily). The pre-existing
aggregateparameter had the identical wrong-type gap (a number/array/object/boolean silently
becamecount) - tracked and fixed the same way in this PR rather than deferred (#4643). A
well-typed but unrecognizedaggregatestring (e.g."bogus") still falls through tocount
unchanged, matching the legacy route's own behavior - that separate, narrower enum-validation
gap is pre-existing, untracked, and deliberately out of scope for this fix. -
GET /api/v1/responses/{id}andGET /api/v1/responses/{id}/exportclamp a caller-supplied
limiton both bounds ([1,1000]and[1,10000]respectively) - the legacyGET /api/responses/{id}/exportroute only floors its own default at 10000; an explicit
?limit=there has no ceiling at all and can attempt an unbounded fetch. That pre-existing
legacy-route bug is deliberately NOT fixed by this PR (the legacy handlers are frozen reference
code here) and is not propagated to the new v1/MCP surfaces; tracked separately as #4310 (broadened
to also cover the plain legacy query route, which shares the same unbounded-limit shape). -
data_export::csv_escapenow neutralizes CSV/formula injection (CWE-1236, #4311) - a leading
=/+/-/@/tab/CR on an agent-controlled field (output,error_detail,plugin) is
prefixed with a literal'before RFC 4180 quoting, so Excel/Sheets renders it as text instead of
executing it as a formula. Promoted fromaccess_review_model.cpp's existing, tested
neutralize_formula/is_formula_trigger(the original precedent for this fix) into the shared
data_export.hppchokepoint - fixes the new v1 export route, the legacyGET /api/responses/{id}/exportroute,access_review_model.cpp's own compliance export, and
data_export::json_array_to_csv's dashboard-driven generic export all at once, and removes the
access-review file's local duplicate of the same logic. -
Approval-review REST v1/MCP parity (#2146 A2-R4).
GET /api/v1/approvals
andGET /api/v1/approvals/pending/countare new versioned twins of the
legacy unversionedGET /api/approvalsandGET /api/approvals/pending/count
(approve/reject stay legacy-only, unchanged, in a separate follow-up); MCP
list_pending_approvalsis widened to the same field set
(reviewed_by/reviewed_at/review_comment), and a new MCP
get_pending_approval_counttool completes the twin. All three read
surfaces share one JSON-row builder (approval_model.hpp) so they cannot
drift from each other.ApprovalManager::query()/pending_count()
previously collapsed a degraded store (not open, pool exhausted, or a
failed query) into the same empty list / zero count a genuinely empty
approval queue returns - a silent false-negative on exactly the signal a
maker-checker workflow depends on. New checked twinsquery_checked()/
pending_count_checked()distinguish the two, so both the REST and MCP
surfaces now answer a genuine store failure with 503/an error envelope
instead of a false "all clear" - a concreteretry_after_mshint on a
transient failure (e.g. pool exhaustion),nullon a permanent one (e.g.
schema drift, disk-full) that will not clear on retry. The underlying
list query is hard-capped at 100 rows; a match count over the cap setsresult_truncated_by_cap
(pagination-nested on REST, top-level on MCP's structured output) rather
than presenting the capped page as the complete queue. -
Guardian (Guaranteed State) REST/MCP write + per-agent read parity (#2146 Batch B1). Seven new MCP tools twin the remaining REST v1 Guaranteed State operations that had no MCP counterpart:
create_guardian_rule,get_guardian_rule,update_guardian_rule, anddelete_guardian_ruletwinPOST/GET/PUT/DELETE /api/v1/guaranteed-state/rules{,/{id}}(samederive_rule_specstructured-authoring validation, samecreate_rule/update_rule/delete_rulestore calls, same fleet-widedeny_fleet_wide_service_scopedposture as their REST siblings — a Guard has no single owning device/service);push_guardian_rulestwinsPOST /api/v1/guaranteed-state/pushvia the identicalGuardianPushFnfan-out closure REST uses (newMcpServer::set_guardian_push_fnseam, wired from the sameserver.cpplambda as REST — the two surfaces cannot dispatch a different push), and is honestly annotatedidempotentHint:false: each call re-dispatches against the current rule set, it does not converge on a no-op replay.get_guardian_agent_statusandget_guardian_device_complianceclose the two per-agent reads #4037 deliberately deferred, twinningGET /api/v1/guaranteed-state/status/{agent_id}andGET /api/v1/guaranteed-state/device-compliancevia the samescoped_perm_fnper-device confinement REST uses (newMcpServer::set_baseline_storeseam for the latter). The per-agent status derivation is also extracted into a new sharedguardian_agent_status_rollupbuilder inguardian_model.hpp(mirroring the #4037guardian_status_rollupprecedent) that both the REST route and its MCP twin now call — incidentally closing a pre-existing REST audit-timing gap where a degrade confined to the second of two sequential store reads was auditedsuccessdespite the request ultimately failing.
Known limitation, tracked separately (#4309, not introduced by this batch): MCP
tier/approval enforcement is architecture-wide inert for an MCP-tier-less caller
(a cookie session, a plain non-MCP-tiered API token, or an engine token -
mcp_tieris only ever set on an actual MCP token) - except for
delete_guardian_rulespecifically, where a governance review round found this
gap meaningfully widened (approval-ticket bypass AND MFA bypass together, on a
destructive deletion of auto-remediation policy - not just an inert approval
step) and a scoped fix landed in the same batch: an MCP-tier-less caller is now
denied outright rather than falling through to RBAC-only enforcement. The
architecture-wide gap remains open forcreate_guardian_rule/
update_guardian_rule/push_guardian_rules(none of which are approval-gated
at any tier -GuaranteedState:Write/Pusharen't inmcp_policy.hpp's
requires_approval()list - so there is no approval to bypass on those three,
only the MFA-step-up gap every other unmigrated approval-gated tool shares) and
every other approval-gated MCP tool not yet migrated. -
Result-set (scope-walking) MCP twins — the full 12-operation REST v1 lifecycle is now reachable via MCP (
list_result_sets,create_result_set,create_result_set_from_inventory_query,create_result_set_from_tar_query,create_result_set_from_instruction_result,reevaluate_result_set,get_result_set,get_result_set_members,get_result_set_lineage,pin_result_set,unpin_result_set,delete_result_set). The three async dispatch producers (create_result_set_from_tar_query/from_instruction_result/reevaluate_result_set) reuse the exact sameExecution:Executegate plus the caller's derived per-device visible set (#1788) that the REST twins use — a service-scoped API token is confined identically on both surfaces. The other nine are owner-scoped (session-authenticated; a service-scoped token is denied outright, matching REST'sdeny_fleet_wide_service_scopedgate) or, forcreate_result_set_from_inventory_query, gated on a realInventory:Readcheck. A new "Result sets" tool family is added to the MCPinitialize.instructionshandshake. -
POST /api/v1/scope/validateandPOST /api/v1/scope/preview— versioned REST v1 twins of thevalidate_scope/preview_scope_targetsMCP tools (the former also twins the legacy unversionedPOST /api/scope/validate), sharing the exact same underlying scope-validation/preview logic as their MCP counterparts so the two transports cannot diverge. -
MCP twins for fleet visualization and execution/fleet statistics.
get_fleet_topologyand
get_host_topologybringGET /api/v1/viz/fleet/topologyandGET /api/v1/viz/host/{id}/topology
to MCP parity — the 3D fleet visualizer's first MCP presence — sharing the offline-host merge rule
(merge_offline_topology) and the M-1machines_maxDoS cap with the REST route.get_execution_statistics,
get_execution_statistics_by_agent,get_execution_statistics_by_definition, andget_fleet_statistics
bringGET /api/v1/execution-statistics{,/agents,/definitions}andGET /api/v1/statisticsto MCP
parity, sharing their JSON-building functions (execution_statistics_model.hpp) with the REST routes
so the two surfaces cannot drift (api-parity programme, #2146 Batch B3). -
Management-group, API-token, RBAC-check, and account-unlock MCP twins (#2146 Batch B4).
Eleven new MCP tools give an agentic worker parity with the remaining/api/v1/management-groups*,
/api/v1/tokens*,/api/v1/rbac/check, and/api/v1/users/{name}/unlockREST v1 surface
(list_management_groups/preview_management_group_agent_count/rotate_api_token/
confirm_api_token_rotationalready had twins):create_management_group,get_management_group,
update_management_group,add_management_group_member,list_management_group_roles,
assign_management_group_role,list_api_tokens,create_api_token,revoke_api_token,
check_permission, andunlock_account. Each mirrors its REST handler's real authorization gate
exactly, verified by reading the full handler rather than inferring from the route path or securable
name:list_management_group_rolesandassign_management_group_roleboth run the REST route's
compound gate (a fleet-wide permission OR the caller already holdingITServiceOwneron the group
in question, the fallback skipped for a service-scoped token) on top oflist_management_group_roles's
own leadingManagementGroup:Readgate (#2376 - the caller must be allowed to see the group AND
allowed to see role assignments);assign_management_group_role
additionally restrictsrole_nametoOperator/Vieweronly, matching REST, with the underlying
store'sRbacStore::validate_assignmentdangerous-role-block chokepoint as defense in depth;
check_permissionmirrorsPOST /api/v1/rbac/check's deliberate zero-RBAC-gate posture (a self-check
of the caller's own authority, open to any authenticated caller);create_api_tokenmirrors the
REST route's multi-store (RbacStore+ManagementGroupStore) authority check for a service-scoped
token;list_api_tokensandcreate_api_tokenare unconditionally self-scoped to the calling
principal, matching REST exactly (there is no admin all-owner-token view on this route - that
capability exists only as an HTMX dashboard fragment with no REST v1 route yet, so no MCP twin).
list_management_group_roles/list_api_tokens/check_permission/get_management_groupare
read-only; the other seven, includingcreate_api_token, are approval-gated at the supervised MCP
tier like every other privileged mutation (ApiToken:Writeis now inmcp_policy.hpp's
supervised-tierrequires_approval()list - closing a gap, shared with the identically-gated REST
route, where a supervised-tier caller could self-mint a fresh, untiered, non-expiring credential
with neither MFA step-up nor human approval;ApiToken:Rotatestays deliberately ungated per its
own documented rationale, which does not transfer to minting a brand-new credential).
McpServer::LockoutClearFn(mirroringRestApiV1::LockoutClearFn) is a new
trailingbuild_handler/register_routesparameter backingunlock_account.
Known limitation, tracked separately (#4309, not introduced by this batch): MCP
tier/approval enforcement is architecture-wide inert for an MCP-tier-less caller
(a cookie session, a plain non-MCP-tiered API token, or an engine token -
mcp_tieris only ever set on an actual MCP token), so the
approval-gating described above applies to MCP-token callers specifically, not
to every caller of the underlying REST route - except forcreate_api_token,
revoke_api_token,unlock_account,rotate_api_token, and
confirm_api_token_rotationspecifically, where a governance review round found
this gap meaningfully widened (raw credential-minting, account-lockout-clearing,
and credential rotation/reveal with no MFA step-up at all - not just an inert
approval step) and a scoped fix landed in the same batch: an MCP-tier-less
caller is now denied outright on these five tools rather than falling through to
RBAC-only enforcement (rotate_api_token/confirm_api_token_rotationwere
fixed in a follow-up review round after the other three, once the identical
pattern was found there too -ApiToken:Rotatewas and remains deliberately
NOT approval-gated per its own documented rationale, so this closes the
missing-step-up gap without adding an approval requirement that was never
intended for rotation). The architecture-wide gap remains open for every other
approval-gated MCP tool (execute_instruction,quarantine_device,
revoke_certificate, and the fourManagementGroup:Writemutations in this
same batch). -
MCP twins for offload targets, CA root-CSR export, CA subordinate-chain import, platform license, and software deployments (#2146 API-parity Batch B5). 14 new MCP tools:
list_offload_targets/create_offload_target/get_offload_target/delete_offload_target/list_offload_target_deliveries(/api/v1/offload-targets*, a complete twin of the family),export_ca_root_csr(GET /api/v1/ca/root-csr- distinct from the genuinely-publicGET /ca/root/GET /ca/crl, which stay REST-only by design),import_ca_chain(POST /api/v1/ca/import-chain- switches the install CA to subordinate mode; Security:Write, approval-gated on the supervised tier),get_platform_license/activate_platform_license/list_license_alerts(/api/v1/license*-DELETE /api/v1/license/{id}has no MCP twin yet, a recorded exception tracked alongside the store's construction below, not an oversight), andlist_software_deployments/create_software_deployment/rollback_software_deployment/cancel_software_deployment(/api/v1/software-deployments*-POST .../startalso has no MCP twin, since it requires a fresh interactive MFA step-up an MCP token cannot satisfy). Each shipped twin uses the exact same permission gate, audit verb, and validation as its REST sibling.LicenseStoreandSoftwareDeploymentStoreremain dormant ondev(ADR-0048/0051, unrelated to this change) - their MCP twins answer "unavailable" until a future PR wires construction, matching the REST routes' own posture today. -
Engine principal class — store, RBAC resolution, and attribution plumbing (engine principals
PR 4.2). A newEnginePrincipalStore(born-on-PG,engine_principal_storeschema) records
autonomous/agentic identities in the reservedengine:<slug>namespace, andRbacStore/
ManagementGroupStoregain the resolution + guard chokepoints that let a fleet-wide engine
principal be granted (non-system) roles and have those roles resolve and attribute correctly in
audit rows. Engine tokens (ApiTokenStore, principal_kind="engine") are referentially checked
against the store at mint time, are alwaysmcp_tier=readonly, and always carry a ≤90-day
expiry — no perpetual, no service-scoped engine tokens. This release ships no operator-facing
surface (no dashboard/REST CRUD for minting or managing engine principals — that lands in PR
4.3) and no scoped (management-group) engine role assignment — engine grants are fleet-wide
only in this release; scoped resolution is a Phase-5 deliverable.
Upgrade note: theengine:prefix is now a reserved namespace for local usernames and local
RBAC group names — the server refuses to start if a pre-existingengine:-named user or
local group is found at boot. See the## ⚠️ Breakingsection in
docs/user-manual/upgrading.mdanddocs/ops-runbooks/engine-principal-store-recovery.mdfor
the pre-upgrade check and recovery procedure. -
Engine-principal lifecycle surface — REST, MCP, and admin console (engine principals
PR 4.3). Nine/api/v1/engine-principalsREST routes (create, list, get, revoke,
mint-credential, rotate-credential, confirm-rotation, transfer-owner, and theaudit/no-admin
auditor) and nine MCP twin tools
give operators (and, for the mutating tools, supervised-tier maker-checker-approved automation)
full lifecycle control over the engine-principal identities introduced in PR 4.1–4.2, plus a new
"Engine Principals" section in the Settings console (create form, list with owner/classification/
active-credential count, mint/rotate buttons behind a one-time secret-reveal panel, revoked rows
showingsuperseded_byand the recorded revoke detail). Credential rotation follows an
overlap-pair model (design doc §7): at most two active credentials per principal, a 24-hour
minimum overlap window (rejected outright, never truncated, below the floor), a ~120-second
grace window that re-serves the same successor secret on a same-caller retry, and a 60-second
background sweep that auto-revokes the predecessor once its overlap window elapses and warns on
an unused successor nearing expiry. A newGET /api/v1/engine-principals/audit/no-admin
auditor (REST + MCPaudit_engine_no_admin) independently proves, by resolving each engine
principal's actual roles and effective permissions against the RBAC reference tables, that "no
admin, ever" holds — literal admin/system-role grants and a full securable×operation wildcard
grant are both checked, and the auditor fails closed (503, "cannot verify") rather than
reporting a false clean bill if RBAC reference data can't be resolved. Every mutating REST route
is admin + MFA-step-up gated; every REST route, including the read routes, structurally denies a
caller authenticated as an engine-classed session (principal_kind="engine"/
auth_source="engine_token") — an engine principal can never touch its own or any other engine
principal's lifecycle surface, not even to list. Deleting a user (dashboard) who owns an active
engine principal is now blocked with409until ownership is transferred; the automated SCIM
deprovision path instead applies a detective control — it always succeeds (a CC6.8 termination is
never blocked) but emits anengine_principal.owner_deprovisionedaudit and a
yuzu_engine_principal_owner_deprovisioned_totalmetric when the departing user still owns active
engine principals, for out-of-band reassignment. -
Fleet-wide engine-principal role assignment (#2202).
GET/POST/DELETE /api/v1/engine-principals/{id}/roles(EnginePrincipal:Readfor the read —
moved offSecurity:Readby #2376 before this entry shipped —Security:Write
for the mutations, which are additionally admin + MFA step-up gated) plus MCP twinslist_engine_roles/assign_engine_role/
unassign_engine_rolelet an admin actually grant an engine principal the fleet-wide RBAC
authority the design promised — without this surface,RbacStore::assign_rolehad no
production caller for theengineprincipal class, so a written grant could never take
effect. Engine principals can never be assignedadmin, any built-in system role, or a
wildcard role — such a request is rejected outright, never silently narrowed. See
docs/user-manual/rest-api.md"Engine Principals" anddocs/user-manual/mcp.mdfor the
full contract. -
ABI4 capability declarations + capability-catalogue rows, group A (PR1.10 group A / PR1.9 data A, #2204).
filesystem(16 actions),tar(14),registry(9),license_scan(2) andvuln_scan(5) — 46 actions total — now declare per-action, per-OSYuzuActionDescriptorlegs onyuzu::Plugin(action_descriptors()/action_descriptor_count()), each leg naming the real acquisition rung (ADR-3002) and mechanism read directly off the implementation's#ifdefstructure, not an aspiration. Newserver/core/src/capability_decls/plugin_action_catalogue_a.hppclassifies 45 of those 46plugin.actionpairs into theCommandCapabilityRegistryvocabulary (DispatchClass/Mutability/securable/operation/risk tier) —tar.fleet_snapshotis intentionally left to the already-landedcore_dispatch_capabilities.hpp, which classifies it as a system-reserved dispatch; declaring it again here would make thatplugin.actionpairAmbiguous. -
ABI4 capability declarations + capability-catalogue rows for the 14 inventory/system plugins (PR1.10 group B / PR1.9 data B, #2204).
hardware,users,status,os_info,storage,installed_apps,processes,procfetch,disk_space,device_identity,agent_logging,diagnostics,example, andchargeneach now overrideaction_descriptors()/action_descriptor_count()with a per-action, per-OSYuzuActionDescriptorarray declaring what the shipped code does today — including the ADR-3002 acquisition rung per leg (native OS call, argv runner, or governed/ungoverned shell) socapmatrix-gencan render real data for this group instead of "undeclared". Newserver/core/src/capability_decls/plugin_action_catalogue_b.hppclassifies all 55 actions for the futureCommandCapabilityRegistry: overwhelminglyReadOnly/Noneon theInventorysecurable, withUserManagementfor theusersplugin,Securityfor the two actions that enumerate TLS certificate/key file paths, andInfrastructure/Executionforstorage's KV mutations andchargen's start/stop. No behavior change — declaration and classification only. -
ABI4 capability declarations + capability-catalogue rows for the network/security plugin group (#2204, PR1.10 group C / PR1.9 data C). The 14 network and security plugins —
network_config,netprobe,netstat,sockwho,network_diag,network_actions,discovery,wifi,wol,http_client,certificates,firewall,quarantine,rdp_control— now declare per-action, per-OS capability legs viaaction_descriptors()/action_descriptor_count()(ABI v4), reflecting what each implementation does today rather than an aspirational target: native in-process APIs (Win32, libproc,/proc, raw sockets, cpp-httplib) are declared at ADR-3002 rung 1; legs routed through the bounded subprocess runner are rung 2; legs that still shell out via a rawpopen()/system()—firewall,quarantine,network_actions,discovery's ping sweep, andwol.checkamong them — are honestly declared rung 3.wifi's macOS Wi-Fi scan path is declaredCONSTRAINEDat rung 3 (it genuinely shells out toairport -s, thensystem_profiler SPAirPortDataType, through the same governed-shell mechanism the Linux leg uses) — neverUNSUPPORTED, which would understate a path that really executes and would misrepresent the macOS ≤13 / Location-Services-granted case, where it works; the limitation (theairportbinary was removed in macOS 14, and the fallback needs Location Services authorisation a background daemon may lack) is recorded as the leg'sfallbacktext instead. Its macOS connected-info leg isCONSTRAINEDat rung 1 (CoreWLAN) noting Location Services can withhold the SSID/BSSID. Newserver/core/src/capability_decls/plugin_action_catalogue_c.hppclassifies all 34 actions across these 14 plugins asCommandCapabilityrows for the PR1.9a command-capability registry —quarantine.quarantineandcertificates.deleteareDestructive, andquarantine.quarantineisIrreversible:unquarantinerestores reachability, but on macOS the quarantine replaces the whole active pf ruleset and the undo restores only the OS default, so any runtime pf rules the endpoint had beforehand are permanently lost — a connectivity-restoration test does not prove state restoration. Declaration and classification only — no plugin behaviour changes. -
ABI4 capability declarations + capability-catalogue rows for the 15 endpoint-management plugins (group D, #2204).
tags,asset_tags,interaction,windows_updates,services,script_exec,software_actions,msi_packages,sccm,wmi,event_logs,antivirus,bitlocker,ioc, andagent_actionseach now declare a per-action, per-OSYuzuActionDescriptorarray (42 actions total) reflecting what the shipped implementation actually does today — never a fabricated claim. Highlights:windows_updatesandbitlockerturn out to have genuine, working Linux/macOS legs despite their names (onlywmiandsccmare truly Windows-only);event_logs' macOSlog showleg is the only rung-2 (argv-runner) leg in the group, everything else that shells out does so at rung 3 (/bin/sh -cor a directly-spawned interpreter such asosascript/powershell/bash);interaction's four osascript-backed dialog actions areCONSTRAINEDon macOS (no reachable GUI session under the root LaunchDaemon);windows_updates.pending_rebootisCONSTRAINEDon macOS (softwareupdate -lis an unbounded network call). Combined with groups A/B/C andcontent_dist, all 49 shipped plugins are now ABI4-declared. Newserver/core/src/capability_decls/plugin_action_catalogue_d.hppclassifies the 41 operator-dispatchable actions from this group (asset_tags.syncis excluded — it issystem_reservedand already carried bycore_dispatch_capabilities.hpp) againstrbac_store.cpp's seeded securables/operations.script_exec's three actions (admin-only arbitrary command/script execution) classifyExecute/Destructive/Irreversibleat the top risk tier;bitlocker.state,msi_packages.list/product_codes,software_actions.list_upgradable/installed_count, andagent_actions.infoclassifyReadOnly— each is a pure status/inventory query with no install, encrypt, or write action in the plugin at all. -
SDK descriptor seam unblocks ABI4 capability declarations for every C++ plugin, and
capmatrix-gengets a completeness check (#2204).YUZU_PLUGIN_EXPORT(sdk/include/yuzu/plugin.hpp) previously built every plugin's descriptor withoutaction_descriptors/action_descriptor_count, so no plugin written through theyuzu::PluginC++ wrapper — all 49 shipped plugins — could ever declare an ABI4 capability leg, regardless of what it implemented.yuzu::Pluginnow exposes two non-pure virtuals with an honest "undeclared" default (nullptr/0), wired straight into the generated descriptor; a plugin overrides both to declare capabilities, and every existing plugin is unaffected.capmatrix-gennow hard-errors, naming both sides, when a declared plugin'sactions()list and itsaction_descriptorsarray disagree — the self-check the five upcoming per-plugin declaration passes depend on. Also fixed:sdk/include/yuzu/plugin.h'srungdoc comment had the acquisition-ladder order (docs/adr/3002-acquisition-ladder.md) inverted — it now correctly states rung 1 = native OS interface (best), 2 = argv runner, 3 = governed shell (worst), and that rung describes how a leg acquires its capability, never how mature the implementation is; a matching legend was added todocs/os-capability-matrix.md. Comment-only — no ABI, struct, or field change. -
capmatrix-gengains an opt-in--registriesmode for the non-plugin capability registries. Guardian's registry/service-guard hive and state lists, TAR's capture-source registry, Spark's per-OS mechanism factories, and DEX'sdex_obs_platforms()coverage map are now each mirrored into a small checked-in table underdocs/capability-registries/, readable bycapmatrix-genvia--registries <dir>without linkingagent_core/server_coreinto the plugin-dlopening host tool. Each table is cross-checked against its live C++ source by a dedicated suite test that fails the moment the two disagree. Existing invocations (the CI drift gate, the shell-test fixtures) are unaffected — the new flag is never passed by either. -
Wake-on-LAN
checkreachability on macOS — thecheckaction now performs a working reachability probe on macOS, bringing it to parity with Linux and Windows. It usesping -t <sec>(the BSD/macOS whole-run deadline) so a live host is correctly reported asreachable. The probe previously used-W, which BSD/macOS interpret as milliseconds rather than seconds, so its effective 2 ms timeout reported essentially every host asunreachable. Linux and Windows behaviour is unchanged. -
macOS DNS parity.
network_actions/flush_dnsnow performs a complete macOS flush —dscacheutil -flushcacheandkillall -HUP mDNSResponder(the SIGHUP is the step that actually resets the resolver) — and reports its status from the real exit codes of both commands instead of a blind success.network_config/dns_cachenow returns an honestunsupportedsentinel on macOS, where the OS exposes no resolver-cache contents to userspace (dscacheutil -cachedumpis defunct on modern macOS), rather than an ambiguous "not available". -
macOS agents now report the live Wi-Fi connection. The
wifi connectedaction reads the current association directly through CoreWLAN — SSID, RSSI, security, BSSID and channel — bringing macOS to parity with the Windows and Linux connection reporting. This supersedes the retiredairport -Ihelper (removed by Apple in macOS 14), which had leftconnectedreporting "Not connected" on every modern Mac even while online. Where macOS 14+ withholds the SSID/BSSID behind Location Services (as it does for a background daemon), the connection is still reported with an<ssid-withheld>marker rather than a false "Not connected". -
macOS internal disk inventory now collected via structured
system_profilerparsing. The agent's disk collector no longer greps rawsystem_profilerkey/value text into malformed rows; it now parsessystem_profiler SPStorageDataType SPNVMeDataType SPSerialATADataType -jsonstructurally to report each internal NVMe/SATA physical disk's model, capacity (GB), media type (SSD/HDD) and interface (NVMe/SATA) as a clean row — the per-volume APFS records underSPStorageDataTypeare consulted only as a medium-type lookup and are never double-counted as disks. This corrects the previously-poisoned device-CIdisks_summarycolumn and removes the collection defect that had deferred the/inventorydevice tab's disk column, though surfacing it in the dashboard itself is a separate, still-deferred change. Scope is internal NVMe/SATA disks only; external USB/Thunderbolt disk coverage is a follow-up. -
Guardian spark 7.7b test seams (#2238). Two rung-7.7a fixes (PR #2236)
landed without regression tests because the behaviours were inert at 7.7a but
become load-bearing at theprefer_sparkcutover. Three gaps close: (1) a
set_rearm_fault_hook_for_testseam onGuardianEngineprovesstart_local()'s
per-rule re-arm loop degrades correctly when one cached rule's re-arm throws
(returns success, logs the failure, the other rules still arm) — previously
nothing could force that throw; (2)started_for_test()introspection on
GuardianOutboxDrainWorker/ConvergenceSchedulerplus engine accessors prove
wire_spark_engine()'sprefer_spark_start gate actually gates thread
start, not just construction — reverting that gate to always-start previously
failed no existing test; (3) a[tsan]-tagged checkpoint starts the drain
worker with a realthis-capturing send and tears the engine down while a
send may be in flight, exercising the race betweenstop()'s join and an
in-flight send for the first time. All three seams are test-only (no
production behaviour change). Items (1) and (2) are mutation-verified: each
goes red against a reversion of the fix it covers, then green again against
the real code. Item (3) is a liveness/TSan checkpoint, not a fix-regression
test — verified green across repeated isolated runs plus the full agent
suite, and under TSan looped 20x clean. -
macOS TAR gains per-connection network quality and event-driven TCP lifecycle. A new
NstatClientreads the privatecom.apple.network.statisticskernel control (no framework, no Apple entitlement) to fill the previous macOSnetqualstub and to drive sub-second TCP connect/close events, with theproc_pidfdinfopoll retained as fallback. System-wide capture requires a root agent; a non-root agent honestly reportscapture_method=nonerather than a partial sample. -
macOS parity for a batch of agent plugins. macOS endpoints now report: installed packages via
pkgutilreceipts (msi_packages, matching the Windows MSI inventory); real network-adapter link speed (network_config, viaSIOCGIFMEDIA) and honest ARP availability; service startup type vialaunchctl(services); last-logon time plus aconsole_stateGUI-login flag (users); FileVault (APFS) disk-encryption status (bitlocker, alongside Windows BitLocker and Linux LUKS); and a dynamically-resolved agent executable path (agent_logging, via_NSGetExecutablePath). -
Honest "unsupported" responses for Windows-only surfaces on macOS.
registry,sccm,rdp_control,interaction, and the agent certificate store now return a structured, honest "not supported on this OS" result on macOS instead of an empty, unknown-action, or fabricated-success response — and a state-changing or unknown action that does nothing now reports terminal FAILURE rather than SUCCESS. -
Persistent, measurable CI history on Big Tam and Wee Tam. Every self-hosted Linux and Windows runner now keeps its own checkout-independent
test-runs.db, recording each workflow attempt, Meson suite result and duration, and recovered known-flake event. Operators can compare platforms and find recurrent flakes with the newci-suite-statsandci-flakesqueries; telemetry is reporting-only and can never change a build or test result. -
Guardian Spark flood-measurement baseline. The
guard.unhealthywire-message
ceiling for a stuck-Unknown rule under Spark's errored-refresh backstop and
priority-lane demotion is now a measured number, not an extrapolation: 1 edge +
288 refreshes/rule/agent/day on the 60s-cadence lanes (service/registry), 1 edge +
180/day on the 600s file lane (accounting for the scheduler's default +/-20%
jitter), replacing a pre-fix ~17k/day estimate that predated both the edge-emission
fix and the refresh/demotion backstop. The previously local-only measurement
harness (a Windows resource sampler + a REST load generator) is now tracked under
docs/spark-rebuild-baselines/. Spark stays dormant in every shipped build;
nothing here changes runtime behavior. -
Fleet-wide Prometheus rollup for the Guardian durable lifecycle-audit journal:
the 22yuzu.guardian_journal_*per-agent heartbeat counters (staging loss,
persist, retention/quarantine, and replay integrity) now sum into unlabelled
yuzu_fleet_guardian_journal_*gauges on every fleet-health sweep. Previously
these integrity and loss signals existed only inside each endpoint's own
heartbeat, so a lost lifecycle audit record was invisible to/metricsand to
any evidence automation that scrapes it. The most important of them,
yuzu_fleet_guardian_journal_evicted_no_send_evidence, counts journal batches
that aged out with no evidence their records were ever transmitted - a potential
CC7.3 integrity gap (the classification is best-effort, so a rise suggests loss
rather than establishing it).The families follow the fleet-rollup absent-not-zero convention: the agent
emits a journal tag only when the counter is non-zero, and the server publishes a
family only if some retained agent reported a value that passed the forged-value
parse, so an absent family reflects that rather than a fabricated0. A flatline
zero would read as "checked, nothing lost" when nothing was checked. Values are
hostile-input parsed (garbage, negative, overlong and implausible all mean "did
not report", never0), so no single agent can destroy a fleet sum with an
overflowing or implausible magnitude. A forged-but-plausible value from an
enrolled agent still sets the gauge - that is inside the heartbeat trust
boundary, which is why the shipped alert templates stay warning-grade. Gauge
names, tag keys and HELP text live in one table that also drives the metric
registration, and a pin test binds that table to the agent's real emitter - with
a test-buildstatic_assertthat turns "added a journal counter but forgot
its fleet gauge" into a compile error.Two meta-signals sit outside that table and publish on every sweep including
at zero:yuzu_fleet_guardian_journal_reporting(the coverage denominator -
0while agents are connected means either the telemetry path is dark or
nothing has been journalled anywhere since restart; note neither meta-signal
detects a stalled sweep, since they are never cleared and retain their last
value) and..._tag_rejected(values
that failed the forged-value parse, which would otherwise be a silent drop). No
alert rules are enabled: no churn-robust new-increment alert exists over an
unlabelled fleet sum of per-agent cumulative counters, so the reviewed group in
docs/prometheus/yuzu-alerts.ymlships commented out - that file carries the full
analysis - and the 22 counters are monitor-only. See
metrics.md → Guardian journal fleet gauges. -
Documented the durable Guardian lifecycle journal's reconnect-replay network traffic (
docs/user-manual/guaranteed-state.md): trigger, rate/burst/per-pass/retention bounds, and the current per-agent-only observability path (no fleet rollup yet, tracked under #2298).prefer_spark-gated and inert in production today. -
Guardian lifecycle-audit journal: three new fleet-integrity gauges (#2298). The journal now surfaces
gauge_underflow(a negative size-gauge read at the write-ceiling check that otherwise camouflages as a healthy empty journal, becausejournal_batch_countclamps a negative to 0 — flip-checklist item 5 / #2303), plussend_exceptionsandbackpressure_drops(per-entry lifecycle-audit delivery throws and enqueue-capacity rejections — governance ledger UP-4). Each is emitted as a sparseyuzu.guardian_*heartbeat tag and rolled up server-side into ayuzu_fleet_guardian_*gauge (monitor-only, like the rest of the family). All three stay 0 and ship no tag while Guardian's Spark detection path is inactive (prefer_spark=false), so the telemetry is in place ahead of that cutover. -
Guardian
/api/v1/guaranteed-state/statusand/status/{agent_id}now return a realerrored_rulescount, derived from the same per-agent compliance census the dashboard's Unhealthy Guards view reads (intersected against the live rule catalogue, so a since-deleted rule's census row is excluded) instead of a hardcoded0; the per-agent route is now scoped and audited (guardian.device.view) the same way asGET /guaranteed-state/device-compliance, so it no longer serves per-device data behind only a global permission check, and the fleet route now denies a service-scoped API token outright rather than admitting it to a fleet-wide aggregate. New unlabelled fleet gaugesyuzu_fleet_guardian_unhealthy_suppressed/_refreshed/_priority_demoted(plus_reporting/_tag_rejectedmeta-signals) roll up the M1 flood-guard heartbeat telemetry (spark #2298).compliant_rules/drifted_rulesremain placeholder0pending full status ingest.
… changelog truncated — this release's changelog exceeds GitHub's 125,000-character release body limit.
See CHANGELOG.md for the full list of changes.
Enterprise installers
| Component | Windows | macOS | Debian/Ubuntu | RHEL/Fedora |
|---|---|---|---|---|
| Agent | .exe (InnoSetup) |
.pkg (launchd) |
.deb (systemd) |
.rpm (systemd) |
| Server | .exe (InnoSetup) |
— | .deb (systemd) |
.rpm (systemd) |
| Gateway | — | — | .deb (systemd) |
.rpm (systemd) |
Windows silent install (GPO / SCCM / Intune)
# Agent
YuzuAgentSetup-*.exe /VERYSILENT /SUPPRESSMSGBOXES /SERVER=yuzu-server:50051 /TOKEN=<enrollment-token>
# Server
YuzuServerSetup-*.exe /VERYSILENT /SUPPRESSMSGBOXES /ADMIN_USER=admin /ADMIN_PASS=<password> `
/HTTPS_CERT=cert.pem /HTTPS_KEY=key.pemSilent uninstall — agent: "C:\Program Files\Yuzu\unins000.exe" /VERYSILENT /SUPPRESSMSGBOXES; server: "C:\Program Files\Yuzu Server\unins000.exe" /VERYSILENT /SUPPRESSMSGBOXES (or the path in the product's UninstallString registry value, if it was installed to another directory). Each removes the program but keeps its data directory: %ProgramData%\Yuzu (the agent's identity and mTLS private key) or %ProgramData%\Yuzu Server (databases, configuration, TLS keys). To decommission a device, revoke its certificate on the server and delete that directory (see docs/agent-bundle.md). Uninstallers from earlier releases (up to 0.14.0-rc2) hang on a silent uninstall; upgrade first.
macOS silent install (Jamf / MDM)
sudo installer -pkg YuzuAgent-*.pkg -target /Linux install
# Debian/Ubuntu
sudo dpkg -i yuzu-agent_*.deb
sudo systemctl edit yuzu-agent # set --server address
# RHEL/Fedora
sudo rpm -i yuzu-agent-*.rpmPlatform archives
| Archive | Platform | Arch |
|---|---|---|
yuzu-linux-x64.tar.gz |
Linux (server + agent) | x86_64 |
yuzu-linux-arm64.tar.gz |
Linux (server + agent) | ARM64 (coming soon) |
yuzu-gateway-linux-x64.tar.gz |
Linux (gateway) | x86_64 |
yuzu-windows-x64.zip |
Windows (server + agent) | x86_64 |
yuzu-macos-arm64.tar.gz |
macOS (server + agent) | ARM64 (Apple Silicon) |
Compose Wizard
yuzu-compose-wizard-*.zip — browser-based step-by-step
generator for a customised docker-compose.yml + .env.
Zero dependencies — extract the zip and open index.html in
a browser. Contributed by
@fjarvis in
#405.
unzip yuzu-compose-wizard-*.zip
cd yuzu-compose-wizard-*
# Open index.html in your browser, or:
python3 -m http.server 8000Docker images
docker pull ghcr.io/DevNullLtd/yuzu-server:v0.14.0
docker pull ghcr.io/DevNullLtd/yuzu-gateway:v0.14.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 archive and installer, and the SBOMs for the server, gateway, postgres and all three chisel images, are listed in the signed SHA256SUMS (#5242). The agent-bundle image builds from the published release, so its SBOM is not part of this release.
Every release ships CycloneDX + SPDX SBOMs per platform
(yuzu-*-{linux-x64,windows-x64,macos-arm64}.{cdx,spdx}.json,
yuzu-{server,gateway,postgres}-image.{cdx,spdx}.json and
yuzu-{server,gateway,agent}-chisel-image.{cdx,spdx}.json), GitHub-native
SLSA build provenance attestations for every binary archive
and Docker image, and cosign keyless signatures on both the
Docker images and the SHA256SUMS manifest. See
docs/user-manual/release-verification.md
for the full verification workflow (gh attestation verify,
cosign verify, CycloneDX / SPDX validation).
See the user manual for full setup instructions.