Skip to content

Yuzu v0.14.0

Choose a tag to compare

@github-actions github-actions released this 03 Oct 22:54
· 4390 commits to main since this release
50a81af

⚠️ Docker Compose users: copy /etc/yuzu/certs out 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 down then up, and up --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 run docker compose down -v, until you have followed the copy-out recipe in docs/user-manual/upgrading.md, section "Docker Compose: copy /etc/yuzu/certs out of the server container before you recreate it". The compose files are fixed on main and 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 in CHANGELOG.md.

Before you upgrade

  • Windows agent: set update-signing options in the service's Environment registry value, not its binary path. Every run of the agent installer, including an upgrade pushed by SCCM, Intune or GPO, rewrites the YuzuAgent service's binary path and silently drops --update-trust-bundle and --update-require-signature from it. OTA updates are then no longer signature-checked. Windows: the service's Environment value in docs/user-manual/server-admin.md gives 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-certs folder 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 repair agent-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).

    PluginConfigStore and UploadGrantStore (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, unauthenticated POST /api/v1/file-retrieval stub (accepted a body-supplied agent_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 target plugin.action no 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/rotate or /rewrap call re-wraps
    plugin secrets too, closing the gap docs/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
    (schema plugin_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 — see docs/adr/3005-plugin-config-store.md. (PR1.5b)

  • Typed schedule parameters + arming re-check on every fire path
    (PR1.5a).
    POST /api/schedules now accepts an optional parameters
    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 a 400). 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 "{}" ScheduleRunner previously 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 +
    target plugin.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-Postgres UploadGrantStore, replacing the trust model of the old
    POST /api/v1/file-retrieval stub — which accepted a client-asserted agent_id and 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.md for 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_*.hpp fragments plus core_dispatch_capabilities.hpp honest 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's actions() override and cross-references it against all six catalogue sources, naming the exact plugin.action when a plugin declares an action with no catalogue row, a fragment declares a row no plugin actually has, or the same plugin.action is declared by more than one source. tests/unit/server/test_capability_catalogue.cpp ([server][dispatch][capability]) composes a CommandCapabilityRegistry over all six sources and asserts the catalogue-wide invariants: every row's risk_tier is at or above its operation's floor, every securable/operation is one rbac_store.cpp actually seeds, every Destructive row is Irreversible, system_reserved is true only for core_dispatch_capabilities.hpp rows, classify() resolves every declared pair, and a locally-constructed duplicate span makes the registry report Ambiguous.

  • Command capability classification core (PR1.9a). New header-only server/core/src/command_capability.hpp / command_capability_parsers.hpp define the plugin.action classification vocabulary (DispatchClass, Mutability) and a CommandCapabilityRegistry that composes several independently-authored capability fragments and classifies a dispatch case-insensitively — an unknown plugin.action is always Unclassified, never a permissive default, and the same plugin.action declared by two fragments is Ambiguous, never first-wins. Also ships the frozen v1|<class>|<mutability>|<plan_hash_hex> dispatch-tag wire grammar (encode/decode) and an order-invariant compute_plan_hash. capability_decls/core_dispatch_capabilities.hpp declares 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.hpp additionally gains three new CapabilitySeed rows (PluginConfig, PluginSecret, UploadGrant), reusing only existing Operation enumerators. RbacStore::seed_defaults() seeds the same three securables into rbac.db — Administrator gets full CRUD via the existing loop, PlatformEngineer gets Read/Write/Delete on all three, and Operator gets Read on PluginConfig/UploadGrant only, never PluginSecret — 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). CommandRequest now carries dispatch_tag (field 9), the opaque v1|<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 vendored agent.proto copies and forwarded untouched on the command-fanout path, the same way the existing Guardian payload field is. Because gpb silently drops any field a gateway build's vendored proto doesn't declare, the gateway now advertises the literal command_dispatch_tag_v1 in StreamStatusNotification.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.yml maintenance run now sweeps leaked yuzu_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's PG_VERSION mtime 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 (find resolved to System32's, not coreutils') and pinned clean: false on the new checkout steps so the weekly job can't wipe the runners' warm build trees.

  • macOS crash reports now carry the application version. .ips crash and hang reports route the app's CFBundleShortVersionString (from the report header app_version, falling back to the body bundleInfo) through the shared canon_version normaliser 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 log predicate that pre-filters the firehose inside logd now requests messageType == "fault" alongside error, so Fault-level signals — notably com.apple.apfs filesystem 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.status gains 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 as yuzu.plugin.tar.* tags. The
    server exposes yuzu_fleet_tar_db_corruption_agents and
    yuzu_fleet_plugin_init_failed{plugin}, writes a tar.db.corruption_quarantined
    audit row per (agent, quarantine identity), deduplicated against the audit store
    and rate-limited per agent per server process, and a new yuzu.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 /dex signal 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.status gains
    <source>_last_status, <source>_consecutive_failures and <source>_last_status_at
    per source, plus procperf_procs_seen and perf_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 metrics yuzu_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}, and yuzu_fleet_spark_{watch_faults,queued_dropped,consumer_errors}. A reviewed yuzu-fleet-spark Prometheus 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_mechanisms counts 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-team senior-led delegation workflow is now committed project-level tooling. The
    skill (.claude/skills/dev-team/) plus its two agents (junior-developer,
    enterprise-architect in .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 Sonnet junior-developer subagents in parallel,
    autonomously resolves their escalations (consulting a Fable enterprise-architect for
    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 HTTP 429 + Retry-After — the A4
    error envelope on REST, a JSON-RPC id: null error (code -32010) on MCP
    — and increments the pre-seeded, bounded-label
    yuzu_server_principal_quota_exhausted_total{side,limit} counter (a
    companion yuzu_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; a consteval site-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_claims stale-claim reconciler are now clock-guarded and SINGLE-WRITER-safe — the
    three prunes via one shared pg::run_clock_guarded_prune helper (the full seven-part guard + a
    pg_try_advisory_xact_lock, reading Postgres now() 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.
    ExecutionTracker gains a third table (event_outbox, migration
    v4) that durably records the transition events it already fans out in-memory
    over ExecutionEventBus — agent-transition, execution-progress, and
    execution-completed — each carrying a global durable monotonic event_id
    (BIGINT IDENTITY). Every event is appended inside the same transaction as
    the state mutation that produced it
    (the agent_exec_status upsert, 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
    the command_execution retention 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 SSE id: remains the reconnect-safe per-channel
    counter (unchanged) — the durable global event_outbox.event_id is 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 by event_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-backed LeaderElector — 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_state table); 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/Groups now accepts
    POST/GET/PUT/PATCH/DELETE (list with pagination and
    filter=displayName eq "..."), and --scim-admin-group
    (YUZU_SCIM_ADMIN_GROUP) grants role=admin to 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 (see docs/user-manual/scim-provisioning.md "Groups → role
    mapping").

  • yuzu_http_requests_total{principal_class="engine"} is now live (engine principals PR 4.5). The principal_class label on HTTP request counts previously withheld engine — a resolved engine-principal request was misclassified as agent (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): engine is emitted for a successfully RESOLVED engine-principal session, while human/agent/none remain presentation-based fallbacks — a request bearing an engine token whose session resolution fails still classifies as agent, never engine. The closed-set pre-seed at startup now covers all four values. See docs/observability-conventions.md for the hybrid-basis contract.

  • RHEL 9 / Rocky 9 / AlmaLinux 9 build support. scripts/setup-rhel9.sh provisions 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 --check verify-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; only meson/native/linux-gcc13.ini is usable (the gcc14 file hard-codes
    gcc-14/g++-14 binary names that Software Collections do not provide, and the gcc15/clang21
    files require mold); postgresql-setup --initdb leaves host auth on ident; and the test role
    needs pg_signal_backend on top of CREATEDB, without which PostgresTestDb'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/fleet and the MCP get_network_fleet tool now return available_keys,
    the fleet's tag keys for the cohort-picker UI — previously only the /network dashboard 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/cohorts route; 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 same version parameter 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-logged dex.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
    PropertiesChanged match on Linux, a real NotifyServiceStatusChangeW
    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 via PUT /api/v1/rbac/enforcement and 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-wide Administrator
    grant, disabling requires the caller's own local account to hold the admin role —
    each refusal names the exact follow-up call to make. The guard runs inside the same
    transaction as the flag write, under the rbac_meta row lock; the enable direction's
    destination check additionally takes the identical FOR UPDATE OF pr lock 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 new yuzu_server_rbac_enforcement_enabled gauge, a
    yuzu_server_rbac_enforcement_toggle_total outcome counter, and both a threshold-free
    YuzuRbacEnforcementChanged alert and a direction-aware YuzuRbacEnforcementDisabled
    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-empty mcp_tier outright, 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}/assignments and MCP
    twins assign_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.

  • /dex Catalogue family cards get a public REST + MCP resource. GET /api/v1/dex/catalogue and the MCP twin get_dex_catalogue now serve the
    per-family monitored/total type count, health-score slice, window event
    count, and busiest member type the /dex Catalogue grid has always
    computed, plus an uncatalogued[] 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
    answers 503 (retryable) rather than a fabricated healthy, zero-event
    catalogue. GET /api/v1/dex/overview and its MCP twin get_dex_overview
    additionally carry connected_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/group and its MCP twin get_dex_catalogue_group
    additionally carry benign, 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 /network throughput. 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 /network throughput 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) plus secret.decrypt_failure are 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_profiles enumerates local Windows user profiles. Reads HKLM\...\ProfileList and reports each profile's SID, resolved name, profile path, and whether its registry hive is currently loaded under HKEY_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_exclusions action. 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 typed permission_denied row rather than a silent empty list.

  • firewall plugin's Linux backend probe gains a real nftables leg. state/rules now read nftables directly over NETLINK_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 in agents/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-gen regenerates the generated parts (and the user-manual plugin index, the docs-site navigation and a content/plugin-docs/<name>.json manifest) from the committed sources with no build; tools/plugin-capture writes stamped samples through the agent's real dispatcher and plugin lifecycle, and the meson docs suite plus a docs-lint touch rule keep both honest. The manifests are embedded in the server and served as GET /api/v1/discover/plugin-docs and the MCP resource yuzu://plugin-docs, with a per-plugin docs summary joined into discover_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.md A3), 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 /governance pass. 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 autoruns agent plugin — list and catalog. 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 offline RegLoadKeyW fallback under SeBackup/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-2 systemctl list-timers fallback 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. catalog performs 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. list runs 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 as constrained with a named reason, never folded into "absent" or a false disabled/enabled; a per-source constrained outcome 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. Security securable, 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 reported constrained (reason startup_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.txt was 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 its list action's [result_status] trailing line was hand-corrected to the value this build's code and tests prove it now produces (any constrained row aggregates to CONSTRAINED/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 tags yuzu.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) and yuzu.guardian_io_arm_disarm_rejected_ceiling
    (a monitor-only counter of R5.1's physical-orphan admission ceiling), each rolled
    up fleet-wide as yuzu_fleet_guardian_arm_pending / yuzu_fleet_guardian_arm_failed
    / yuzu_fleet_guardian_io_arm_disarm_rejected_ceiling. Dormant while prefer_spark
    is off (every released agent today).

  • New read-only app_usage agent plugin — summary, last_used, foreground. Machine-scope application-usage evidence derived from TAR's default-on usage fold (usage_daily/usage_daily_user/usage_live inside tar.db, read-only, PRAGMA query_only): summary returns one row per executable over a window (days 1-365, top 1-500, by run_time|run_count) — run count, total run seconds, first/last seen, a distinct-user COUNT (never a user name), superseded/expired runs — preceded by a meta| row carrying the fold's own health counters; last_used returns per-executable first/last-seen within TAR's retained usage window (31 days by default) plus a trailing-30-day window, optionally narrowed by exe. 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 always constrained on every OS — session attribution is the user-context-bridge programme's, not this plugin's. A disabled usage source, an older TAR schema, or an unreadable or corrupted tar.db report constrained|<reason>, never an empty result; a stalled TAR usage/process collector additionally gates last_used the same way (summary instead flags it via its meta| row's feeder_enabled field). last_used's unfiltered form is capped at 5000 executables, reporting a trailing constrained|last_used_truncated row 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). Forensics securable, AdminOrApproval, single-target, audited. A new ADR-0016 app_usage daily-sync source ships the last_used projection 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-usage and its MCP twin get_agent_app_usage return one device's per-executable usage projection — exe_key, first_seen/last_seen within TAR's retained usage window (31 days by default), and a trailing-30-day run_count_30d/total_seconds_30d — ingested from the agent's daily sync (app_usage source, ADR-0016; hash-skip over the raw received blob bytes, software_licensing precedent) once the agent-side producer ships. Server half only in this change — the agent-side app_usage SyncSource lands 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 for VulnFindingStore (docs/postgres-migration-ladder.md). No user names, pids or command lines: the store carries none. Both surfaces require Forensics:Read scoped to the device (tier + management group, ancestor-aware) — and, on an installation with RBAC off, an Administrator session (Forensics:Read joins the topology floor, matching the securable's documented Administrator-only posture) — emit a per-open app_usage.agent.view audit and FAIL CLOSED (503, Sec-Audit-Failed: true) when that audit cannot persist; a store degrade is 503, never an empty 200. The new app_usage_store Postgres schema joins the decommission cascade (DELETE /api/v1/sle/agents/{id}, now six stores) and the yuzu_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_artifacts agent 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/11 10ts layout, path + last-modified per entry), Amcache.hve InventoryApplicationFile (path, SHA-1, size, link date, publisher, binary type — the live hive is copied then loaded privately via RegLoadAppKeyW, serialised by the shared offline-hive lock), and C:\Windows\Prefetch\*.pf (MAM/XPRESS-Huffman decompressed in-process via ntdll RtlDecompressBufferEx; 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 report unsupported|windows_only_artefact. Every action is on the Forensics securable, 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 with PUT /api/v1/plugin-config/execution_artifacts/kill-switch. A truncated, malformed, unknown-version or over-cap artefact is reported constrained with 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/hardware and GET /api/v1/hardware/{id} are the REST twins.

  • The standalone Devices page is now part of Hardware. /devices and /device?id= redirect to /hardware and /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 on Inventory:Read rather than the old page's Infrastructure:Read; every seeded role holding one already holds the other, but a custom role granted only Infrastructure:Read loses list access on upgrade (see the Upgrading guide's "Hardware CI list requires Inventory: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.md is 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,
    the device_class consent gate). proto/yuzu/reflex/v1/reflex.proto defines the wire schema for
    Reflex Set deploy/status; GuaranteedStateEvent gains an additive family field 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-addr option (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-addr for any
    deployment that doesn't configure per-cluster addresses (no change in behavior for existing
    single-cluster installs). A malformed or duplicate --gateway-cluster-addr entry 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). AgentRegistry now merges a durable Postgres-backed presence signal (OfflineEndpointStore, extended with a session-guarded liveness row) into all_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 in not_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 twin export_access_review, GET /api/v1/access-reviews/GET /api/v1/access-reviews/{id} (and their MCP twins), and the frozen campaign row POST /api/v1/access-reviews persists all carry a new rbac_enforcement field — 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. See docs/user-manual/rest-api.md's rbac_enforcement section for the exact three-value semantics and docs/user-manual/rbac.md for 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/assignments and its
    MCP twin list_rbac_role_assignments return 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 dedicated AccessReview:Read
    securable the SOC 2 access-review export uses, admitting the seeded Reviewer role 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 in docs/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 ::warning with 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 #N now works on dev merges (ADR-3001 pillar 2, #2139). A new
    close-linked-issues workflow runs on every push to dev, parses the merged PRs' bodies with
    the repo's single closing-keyword grammar (scripts/tracker/closing_refs.py — GitHub's keywords
    plus comma/and chains, code-fence/quote stripping, and negation suppression, acceptance-tested
    against GitHub's own closingIssuesReferences oracle with zero false negatives), and closes the
    claimed issues as completed with 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 a needs-triage issue rather than closing
    in bulk. Liveness is three mechanisms: an if: failure() alert job (opens/updates an
    automation-broken issue, 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-push for exact-batch reversal.
    A deterministic zizmor guard fails any PR that deletes the workflow, adds a paths: filter,
    moves it off the push: trigger, or adds any PR-context/dispatch/cron trigger.

  • Issue-lifecycle standard adopted (ADR-3001, amended A1). Filing, labelling, and closing
    GitHub issues now follow docs/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 a push-to-dev trigger, the triage sweep loses its autonomous
    closure tier, the per-PR close cap drops to 6, and the CODEOWNERS artefact is descoped. The
    /governance and /test skills now file deferred findings per the standard (dedupe-first,
    governance-deferred label, 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_status field parity, and
    list_schedules/GET /api/v1/schedules filters
    (api-parity programme, #2146 A2-R1). New
    route GET /api/v1/executions/{id}/children and new MCP tool get_execution_children twin the
    legacy GET /api/executions/{id}/children - same Execution:Read fleet-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 shared execution_child_row_json builder
    (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
    like scope_expression already was) plus completed_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/schedules and
    MCP list_schedules gain optional definition_id/enabled_only filters. enabled_only is a
    real boolean on both (gov docs-writer/cpp-expert fix round: the initial parse reproduced the
    legacy GET /api/schedules route'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/policies before this PR shipped, matching MCP list_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, and GET /api/v1/responses/{id}/export bring the legacy, unversioned GET /api/responses/{id}* family
    to REST+MCP parity - same Response:Read fleet-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-keyed GET /api/v1/executions/{id}/responses.
    GET /api/v1/responses/{id} and MCP query_responses share one JSON row builder
    (response_query_row_json); GET /api/v1/responses/{id}/aggregate and MCP aggregate_responses
    share another (response_aggregate_row_json) - both new, in response_query_model.{hpp,cpp},
    per docs/api-twin-recipe.md Rule 1.

  • query_responses rows now carry id, instruction_id, error_detail, plugin, and
    received_at_ms
    , alongside the pre-existing agent_id/execution_id/status/output/
    timestamp - all were already present on StoredResponse but never surfaced to an MCP caller.

  • aggregate_responses gained an op_column parameter (timestamp/status/id, default
    id), validated against ResponseStore::allowed_op_column(). Previously op_column was never
    read from the tool's arguments at all, so a sum/avg/min/max aggregate silently operated
    on the store's own default operand column regardless of what a caller requested. A
    present-but-wrong-JSON-type op_column (e.g. a number) is rejected with kInvalidParams
    rather than silently read as absent and defaulted to id (same defect class as #2970B/#2146
    A2-R1's param_int_strict/param_bool_strict/param_string_strict family). The pre-existing
    aggregate parameter had the identical wrong-type gap (a number/array/object/boolean silently
    became count) - tracked and fixed the same way in this PR rather than deferred (#4643). A
    well-typed but unrecognized aggregate string (e.g. "bogus") still falls through to count
    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} and GET /api/v1/responses/{id}/export clamp a caller-supplied
    limit on both bounds ([1,1000] and [1,10000] respectively) - the legacy GET /api/responses/{id}/export route 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_escape now 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 from access_review_model.cpp's existing, tested
    neutralize_formula/is_formula_trigger (the original precedent for this fix) into the shared
    data_export.hpp chokepoint - fixes the new v1 export route, the legacy GET /api/responses/{id}/export route, 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
    and GET /api/v1/approvals/pending/count are new versioned twins of the
    legacy unversioned GET /api/approvals and GET /api/approvals/pending/count
    (approve/reject stay legacy-only, unchanged, in a separate follow-up); MCP
    list_pending_approvals is widened to the same field set
    (reviewed_by/reviewed_at/review_comment), and a new MCP
    get_pending_approval_count tool 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 twins query_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 concrete retry_after_ms hint on a
    transient failure (e.g. pool exhaustion), null on 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 sets result_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, and delete_guardian_rule twin POST/GET/PUT/DELETE /api/v1/guaranteed-state/rules{,/{id}} (same derive_rule_spec structured-authoring validation, same create_rule/update_rule/delete_rule store calls, same fleet-wide deny_fleet_wide_service_scoped posture as their REST siblings — a Guard has no single owning device/service); push_guardian_rules twins POST /api/v1/guaranteed-state/push via the identical GuardianPushFn fan-out closure REST uses (new McpServer::set_guardian_push_fn seam, wired from the same server.cpp lambda as REST — the two surfaces cannot dispatch a different push), and is honestly annotated idempotentHint:false: each call re-dispatches against the current rule set, it does not converge on a no-op replay. get_guardian_agent_status and get_guardian_device_compliance close the two per-agent reads #4037 deliberately deferred, twinning GET /api/v1/guaranteed-state/status/{agent_id} and GET /api/v1/guaranteed-state/device-compliance via the same scoped_perm_fn per-device confinement REST uses (new McpServer::set_baseline_store seam for the latter). The per-agent status derivation is also extracted into a new shared guardian_agent_status_rollup builder in guardian_model.hpp (mirroring the #4037 guardian_status_rollup precedent) 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 audited success despite 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_tier is only ever set on an actual MCP token) - except for
    delete_guardian_rule specifically, 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 for create_guardian_rule/
    update_guardian_rule/push_guardian_rules (none of which are approval-gated
    at any tier - GuaranteedState:Write/Push aren't in mcp_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 same Execution:Execute gate 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's deny_fleet_wide_service_scoped gate) or, for create_result_set_from_inventory_query, gated on a real Inventory:Read check. A new "Result sets" tool family is added to the MCP initialize.instructions handshake.

  • POST /api/v1/scope/validate and POST /api/v1/scope/preview — versioned REST v1 twins of the validate_scope/preview_scope_targets MCP tools (the former also twins the legacy unversioned POST /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_topology and
    get_host_topology bring GET /api/v1/viz/fleet/topology and GET /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-1 machines_max DoS cap with the REST route. get_execution_statistics,
    get_execution_statistics_by_agent, get_execution_statistics_by_definition, and get_fleet_statistics
    bring GET /api/v1/execution-statistics{,/agents,/definitions} and GET /api/v1/statistics to 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}/unlock REST v1 surface
    (list_management_groups/preview_management_group_agent_count/rotate_api_token/
    confirm_api_token_rotation already 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, and unlock_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_roles and assign_management_group_role both run the REST route's
    compound gate (a fleet-wide permission OR the caller already holding ITServiceOwner on the group
    in question, the fallback skipped for a service-scoped token) on top of list_management_group_roles's
    own leading ManagementGroup:Read gate (#2376 - the caller must be allowed to see the group AND
    allowed to see role assignments); assign_management_group_role
    additionally restricts role_name to Operator/Viewer only, matching REST, with the underlying
    store's RbacStore::validate_assignment dangerous-role-block chokepoint as defense in depth;
    check_permission mirrors POST /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_token mirrors the
    REST route's multi-store (RbacStore + ManagementGroupStore) authority check for a service-scoped
    token; list_api_tokens and create_api_token are 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_group are
    read-only; the other seven, including create_api_token, are approval-gated at the supervised MCP
    tier like every other privileged mutation (ApiToken:Write is now in mcp_policy.hpp's
    supervised-tier requires_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:Rotate stays deliberately ungated per its
    own documented rationale, which does not transfer to minting a brand-new credential).
    McpServer::LockoutClearFn (mirroring RestApiV1::LockoutClearFn) is a new
    trailing build_handler/register_routes parameter backing unlock_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_tier is 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 for create_api_token,
    revoke_api_token, unlock_account, rotate_api_token, and
    confirm_api_token_rotation specifically, 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_rotation were
    fixed in a follow-up review round after the other three, once the identical
    pattern was found there too - ApiToken:Rotate was 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 four ManagementGroup:Write mutations 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-public GET /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), and list_software_deployments/create_software_deployment/rollback_software_deployment/cancel_software_deployment (/api/v1/software-deployments* - POST .../start also 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. LicenseStore and SoftwareDeploymentStore remain dormant on dev (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 new EnginePrincipalStore (born-on-PG, engine_principal_store schema) records
    autonomous/agentic identities in the reserved engine:<slug> namespace, and RbacStore/
    ManagementGroupStore gain 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 always mcp_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: the engine: prefix is now a reserved namespace for local usernames and local
    RBAC group names — the server refuses to start if a pre-existing engine:-named user or
    local group is found at boot. See the ## ⚠️ Breaking section in
    docs/user-manual/upgrading.md and docs/ops-runbooks/engine-principal-store-recovery.md for
    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-principals REST routes (create, list, get, revoke,
    mint-credential, rotate-credential, confirm-rotation, transfer-owner, and the audit/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
    showing superseded_by and 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 new GET /api/v1/engine-principals/audit/no-admin
    auditor (REST + MCP audit_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 with 409 until 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 an engine_principal.owner_deprovisioned audit and a
    yuzu_engine_principal_owner_deprovisioned_total metric 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:Read for the read —
    moved off Security:Read by #2376 before this entry shipped — Security:Write
    for the mutations, which are additionally admin + MFA step-up gated) plus MCP twins list_engine_roles/assign_engine_role/
    unassign_engine_role let an admin actually grant an engine principal the fleet-wide RBAC
    authority the design promised — without this surface, RbacStore::assign_role had no
    production caller for the engine principal class, so a written grant could never take
    effect. Engine principals can never be assigned admin, 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" and docs/user-manual/mcp.md for 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) and vuln_scan (5) — 46 actions total — now declare per-action, per-OS YuzuActionDescriptor legs on yuzu::Plugin (action_descriptors()/action_descriptor_count()), each leg naming the real acquisition rung (ADR-3002) and mechanism read directly off the implementation's #ifdef structure, not an aspiration. New server/core/src/capability_decls/plugin_action_catalogue_a.hpp classifies 45 of those 46 plugin.action pairs into the CommandCapabilityRegistry vocabulary (DispatchClass/Mutability/securable/operation/risk tier) — tar.fleet_snapshot is intentionally left to the already-landed core_dispatch_capabilities.hpp, which classifies it as a system-reserved dispatch; declaring it again here would make that plugin.action pair Ambiguous.

  • 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, and chargen each now override action_descriptors()/action_descriptor_count() with a per-action, per-OS YuzuActionDescriptor array declaring what the shipped code does today — including the ADR-3002 acquisition rung per leg (native OS call, argv runner, or governed/ungoverned shell) so capmatrix-gen can render real data for this group instead of "undeclared". New server/core/src/capability_decls/plugin_action_catalogue_b.hpp classifies all 55 actions for the future CommandCapabilityRegistry: overwhelmingly ReadOnly/None on the Inventory securable, with UserManagement for the users plugin, Security for the two actions that enumerate TLS certificate/key file paths, and Infrastructure/Execution for storage's KV mutations and chargen'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 via action_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 raw popen()/system() — firewall, quarantine, network_actions, discovery's ping sweep, and wol.check among them — are honestly declared rung 3. wifi's macOS Wi-Fi scan path is declared CONSTRAINED at rung 3 (it genuinely shells out to airport -s, then system_profiler SPAirPortDataType, through the same governed-shell mechanism the Linux leg uses) — never UNSUPPORTED, which would understate a path that really executes and would misrepresent the macOS ≤13 / Location-Services-granted case, where it works; the limitation (the airport binary was removed in macOS 14, and the fallback needs Location Services authorisation a background daemon may lack) is recorded as the leg's fallback text instead. Its macOS connected-info leg is CONSTRAINED at rung 1 (CoreWLAN) noting Location Services can withhold the SSID/BSSID. New server/core/src/capability_decls/plugin_action_catalogue_c.hpp classifies all 34 actions across these 14 plugins as CommandCapability rows for the PR1.9a command-capability registry — quarantine.quarantine and certificates.delete are Destructive, and quarantine.quarantine is Irreversible: unquarantine restores 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, and agent_actions each now declare a per-action, per-OS YuzuActionDescriptor array (42 actions total) reflecting what the shipped implementation actually does today — never a fabricated claim. Highlights: windows_updates and bitlocker turn out to have genuine, working Linux/macOS legs despite their names (only wmi and sccm are truly Windows-only); event_logs' macOS log show leg is the only rung-2 (argv-runner) leg in the group, everything else that shells out does so at rung 3 (/bin/sh -c or a directly-spawned interpreter such as osascript/powershell/bash); interaction's four osascript-backed dialog actions are CONSTRAINED on macOS (no reachable GUI session under the root LaunchDaemon); windows_updates.pending_reboot is CONSTRAINED on macOS (softwareupdate -l is an unbounded network call). Combined with groups A/B/C and content_dist, all 49 shipped plugins are now ABI4-declared. New server/core/src/capability_decls/plugin_action_catalogue_d.hpp classifies the 41 operator-dispatchable actions from this group (asset_tags.sync is excluded — it is system_reserved and already carried by core_dispatch_capabilities.hpp) against rbac_store.cpp's seeded securables/operations. script_exec's three actions (admin-only arbitrary command/script execution) classify Execute/Destructive/Irreversible at the top risk tier; bitlocker.state, msi_packages.list/product_codes, software_actions.list_upgradable/installed_count, and agent_actions.info classify ReadOnly — 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-gen gets a completeness check (#2204). YUZU_PLUGIN_EXPORT (sdk/include/yuzu/plugin.hpp) previously built every plugin's descriptor without action_descriptors/action_descriptor_count, so no plugin written through the yuzu::Plugin C++ wrapper — all 49 shipped plugins — could ever declare an ABI4 capability leg, regardless of what it implemented. yuzu::Plugin now 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-gen now hard-errors, naming both sides, when a declared plugin's actions() list and its action_descriptors array disagree — the self-check the five upcoming per-plugin declaration passes depend on. Also fixed: sdk/include/yuzu/plugin.h's rung doc 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 to docs/os-capability-matrix.md. Comment-only — no ABI, struct, or field change.

  • capmatrix-gen gains an opt-in --registries mode 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's dex_obs_platforms() coverage map are now each mirrored into a small checked-in table under docs/capability-registries/, readable by capmatrix-gen via --registries <dir> without linking agent_core/server_core into 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 check reachability on macOS — the check action now performs a working reachability probe on macOS, bringing it to parity with Linux and Windows. It uses ping -t <sec> (the BSD/macOS whole-run deadline) so a live host is correctly reported as reachable. The probe previously used -W, which BSD/macOS interpret as milliseconds rather than seconds, so its effective 2 ms timeout reported essentially every host as unreachable. Linux and Windows behaviour is unchanged.

  • macOS DNS parity. network_actions/flush_dns now performs a complete macOS flush — dscacheutil -flushcache and killall -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_cache now returns an honest unsupported sentinel on macOS, where the OS exposes no resolver-cache contents to userspace (dscacheutil -cachedump is defunct on modern macOS), rather than an ambiguous "not available".

  • macOS agents now report the live Wi-Fi connection. The wifi connected action 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 retired airport -I helper (removed by Apple in macOS 14), which had left connected reporting "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_profiler parsing. The agent's disk collector no longer greps raw system_profiler key/value text into malformed rows; it now parses system_profiler SPStorageDataType SPNVMeDataType SPSerialATADataType -json structurally 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 under SPStorageDataType are consulted only as a medium-type lookup and are never double-counted as disks. This corrects the previously-poisoned device-CI disks_summary column and removes the collection defect that had deferred the /inventory device 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 the prefer_spark cutover. Three gaps close: (1) a
    set_rearm_fault_hook_for_test seam on GuardianEngine proves start_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/ConvergenceScheduler plus engine accessors prove
    wire_spark_engine()'s prefer_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 real this-capturing send and tears the engine down while a
    send may be in flight, exercising the race between stop()'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 NstatClient reads the private com.apple.network.statistics kernel control (no framework, no Apple entitlement) to fill the previous macOS netqual stub and to drive sub-second TCP connect/close events, with the proc_pidfdinfo poll retained as fallback. System-wide capture requires a root agent; a non-root agent honestly reports capture_method=none rather than a partial sample.

  • macOS parity for a batch of agent plugins. macOS endpoints now report: installed packages via pkgutil receipts (msi_packages, matching the Windows MSI inventory); real network-adapter link speed (network_config, via SIOCGIFMEDIA) and honest ARP availability; service startup type via launchctl (services); last-logon time plus a console_state GUI-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 new ci-suite-stats and ci-flakes queries; telemetry is reporting-only and can never change a build or test result.

  • Guardian Spark flood-measurement baseline. The guard.unhealthy wire-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 22 yuzu.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 /metrics and 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 fabricated 0. 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", never 0), 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-build static_assert that 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 -
    0 while 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.yml ships 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, because journal_batch_count clamps a negative to 0 — flip-checklist item 5 / #2303), plus send_exceptions and backpressure_drops (per-entry lifecycle-audit delivery throws and enqueue-capacity rejections — governance ledger UP-4). Each is emitted as a sparse yuzu.guardian_* heartbeat tag and rolled up server-side into a yuzu_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/status and /status/{agent_id} now return a real errored_rules count, 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 hardcoded 0; the per-agent route is now scoped and audited (guardian.device.view) the same way as GET /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 gauges yuzu_fleet_guardian_unhealthy_suppressed / _refreshed / _priority_demoted (plus _reporting / _tag_rejected meta-signals) roll up the M1 flood-guard heartbeat telemetry (spark #2298). compliant_rules/drifted_rules remain placeholder 0 pending 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.pem

Silent uninstall — agent: "C:\Program Files\Yuzu\unins000.exe" /VERYSILENT /SUPPRESSMSGBOXES; server: "C:\Program Files\Yuzu Server\unins000.exe" /VERYSILENT /SUPPRESSMSGBOXES (or the path in the product's UninstallString registry value, if it was installed to another directory). Each removes the program but keeps its data directory: %ProgramData%\Yuzu (the agent's identity and mTLS private key) or %ProgramData%\Yuzu Server (databases, configuration, TLS keys). To decommission a device, revoke its certificate on the server and delete that directory (see docs/agent-bundle.md). Uninstallers from earlier releases (up to 0.14.0-rc2) hang on a silent uninstall; upgrade first.

macOS silent install (Jamf / MDM)

sudo installer -pkg YuzuAgent-*.pkg -target /

Linux install

# Debian/Ubuntu
sudo dpkg -i yuzu-agent_*.deb
sudo systemctl edit yuzu-agent  # set --server address

# RHEL/Fedora
sudo rpm -i yuzu-agent-*.rpm

Platform archives

Archive Platform Arch
yuzu-linux-x64.tar.gz Linux (server + agent) x86_64
yuzu-linux-arm64.tar.gz Linux (server + agent) ARM64 (coming soon)
yuzu-gateway-linux-x64.tar.gz Linux (gateway) x86_64
yuzu-windows-x64.zip Windows (server + agent) x86_64
yuzu-macos-arm64.tar.gz macOS (server + agent) ARM64 (Apple Silicon)

Compose Wizard

yuzu-compose-wizard-*.zip — browser-based step-by-step
generator for a customised docker-compose.yml + .env.
Zero dependencies — extract the zip and open index.html in
a browser. Contributed by
@fjarvis in
#405.

unzip yuzu-compose-wizard-*.zip
cd yuzu-compose-wizard-*
# Open index.html in your browser, or:
python3 -m http.server 8000

Docker images

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

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

Verify checksums

sha256sum -c SHA256SUMS

Verify signature (cosign)

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

Supply chain: SBOM, SLSA provenance, signed images

Every release 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.