Repository navigation
Releases: DevNullLtd/Yuzu
Release list
Yuzu v0.15.0
Added
-
local_security_policy plugin: Windows leg and the
sudoersaction. Windows readspassword_policy,lockout_policyandaudit_policyfrom one read-onlysecedit /export /areas SECURITYPOLICY(argv leaf, no shell,argv[0]under the resolved system directory); the export is staged in an agent-owned, owner-only scratch directory underagent.data_dir, removed on return and swept by a later dispatch if orphaned, and an export that does not end in the signed[Version]section reportssecedit:export_incomplete, never rows.sudoers(Linux and macOS) reads/etc/sudoersand/etc/sudoers.d/*, lexed as sudo 1.9.16 lexes them, and reports one row per rule clause; an unreadable file ispermission_denied/unreadable, never an empty result, and Windows reports a fixedwindows_has_no_sudoersrow. The action isSecurityread-only with a Medium risk tier. -
privacy_permissions macOS leg. The macOS leg now reads per-app camera, microphone and full-disk-access grants from the system and every real per-user
TCC.db(location is administered outside TCC and reports an explicitunsupportedrow), read-only and in-process (never asqlite3CLI shellout), over an immutable descriptor with bounded schema/row/value/byte/time reads; it replaces the singleunsupported/macos:plannedplaceholder row released in 0.14.0. The plugin is still off until an operator enables it. An agent identity without Full Disk Access readsdeniedfor every source and reportsPERMISSION_DENIED; the production root LaunchDaemon's outcome and how to grant it Full Disk Access are not yet measured, and per-user rows report what that user's ownTCC.dbrecords, not whattccdenforces. Windows ships in the same release (see the privacy_permissions Windows leg entry). -
privacy_permissions Windows leg. Reads each real profile's
CapabilityAccessManagerConsentStore (the loaded HKU hive first, else the profile'sNTUSER.DATunderSeBackup/SeRestore) and the HKLM device toggle for camera, microphone, location and full-disk-access, the most restrictive value winning, with per-app last-used times; it replaces theunsupported/windows:plannedplaceholder row listed under 0.14.0. The offlineNTUSER.DATload is refused for a UNC, non-fixed-drive, reparse-point or reparse-ancestor, redirected, offline-placeholder, oversized or foreign-owned hive file, or one with a reparse-point or hard-linked transaction-log sidecar, and a file whose identity changes across the load is unloaded unread; a ConsentStore change reported during the read refuses that source rather than guessing. The 15 s deadline is cooperative, not a hard cap. The plugin is still off until an operator enables it; the switch is per plugin, not per OS, so a fleet that already enabled it for Linux or macOS reads Windows profiles on upgrade (dispatch is stillAdminOrApproval, single-target and audited). An offline mount alters the logged-off profile's hive sequence number and hash. -
The
filesystemplugin'sget_aclaction now reports the access-control entries on Linux and macOS, not just the basic owner, group and permission bits. Linux decodes the POSIX ACL extended attributes (system.posix_acl_accessand, for a directory,system.posix_acl_default) into one row per user, group, mask and other entry, with default entries marked. macOS reads the extended ACL and reports each entry's allow or deny, principal, permissions and inheritance flags. On Windows the descriptor's control flags are reported as acontrolrow, each entry carries its inheritance flags, and a conditional or other unmodelled entry is reported as a row of its own instead of being dropped. A read that fails is reported as unreadable with a reason, never as a file with no ACL. Windowsacerows now carry a fifth field with the inheritance flags, an undecoded ACE type prints-for its mask instead of a zero, and a failedGetAceorGetAclInformationnow ends the command with an error instead of silently returning fewer rows. -
New read-only
mgmt_postureagent plugin — which management plane a device is under, on Linux and macOS (the Windows leg is not read yet). The singlepostureaction reports facts only and changes nothing. On Linux it reads/etc/sssd/sssd.confand/etc/sssd/conf.d/*.conffor the active SSSD domains and reports the plane asad,ipaornone(an IPA realm in/etc/ipa/default.confalso reportsipa), plus whether/etc/krb5.keytabis present; it reports the configuration as written, not the live join state. On macOS it reads MDM enrolment and the MDM server host fromprofiles status -type enrollment. An SSSD configuration the agent is not allowed to read reportspermission_deniedwith planeunknown, never "not joined", and a failed or partial read reports a constrained status with a token instead of an empty result (a macOS tool that cannot be started reports unavailable instead). The Windows leg reports oneunsupportedstatus row (windows:planned) until it ships. -
network_config
routesaction. The network configuration plugin can now return the host's full routing table on Windows, Linux and macOS, IPv4 and IPv6 together: destination prefix, next hop, interface, metric, table, route type and who installed it, capped at 20,000 rows. It reads the table natively and spawns nothing (rtnetlink on Linux, the routing-socket table dump on macOS, the IP Helper forwarding table on Windows). Each OS is reported as constrained because what it hides or reports differs (the host's own addresses, cloned and local entries, a multipath route's extra next hops); the differences are documented per OS. A table that could not be read completely is reported as unavailable or incomplete, never as an empty table: on Linux a dump the kernel aborts, or flags as changed mid-read, is marked incomplete. Same securable, risk tier and approval gate as the othernetwork_configactions; the result definition isdevice.network_config.routes. -
Shared agent primitives in agents/shared: hop-by-hop O_NOFOLLOW directory open and bounded no-follow regular-file read (posix_secure_read.hpp, single-component dir-fd contract), in-memory SHA-256 (sha256.hpp, moved from license_scan) and the [redacted-email] helper (email_redaction.hpp, moved from browser_inventory); no plugin output changes (#4866).
-
Software catalogue MCP twins and raw-route pagination and exports. MCP tools
query_software_catalogueandget_software_versionsshare the REST catalogue's model.GET /api/v1/inventory/softwareandquery_installed_softwaregain aversionfilter, asourcefield per row and opaquecursor/next_cursorpagination; the REST route also gainsq,kind,ecosystemandsourcefilters and aformat=csv|jsonexport of up to 10000 rows (REST-only; a CSV export carries its continuation in anX-Next-Cursorheader and ends with a# result_truncated_by_caprecord when more rows may exist). -
Software catalogue REST routes.
GET /api/v1/inventory/software/cataloguereturns the fleet software title catalogue withtotalsthat cover every row under the active filters, first-page KPIs (headline numbers plus reporting/connected/stale device counts) and aformat=csvtitles export;GET /api/v1/inventory/software/catalogue/versionsreturns installs per version. Keyset cursors are bound to the filter set and to the hourly catalogue refresh (a stale or mismatched cursor is a400), every served page or file is audited, a degraded store (or, for the fleet export and the unfiltered versions read, a catalogue not yet built) is a retryable503, a confined caller is denied the fleet view but may read an in-scope device, and service-scoped tokens are denied. -
Server: application stability score core. A pure, transparent, uncalibrated stability score for an application or one version of it, from the share of reporting devices affected, per-device-capped crash and hang counts, and an optional version-over-version rate-ratio interval. It returns the four deductions (breadth 60, crashes 20, hangs 8, regression 12 points) alongside the 0-100 score and an excellent / good / fair / poor band (90 / 75 / 60), banded on the score rounded to one decimal so the band always matches the displayed value, and withholds the score rather than showing 0 or 100 when the population is too small or the inputs contradict each other. Not surfaced by this entry: no page, route or API reads it here, so this entry alone has no operator-visible change;
GET /api/v1/dex/appsandlist_dex_appsserve the score in the "DEX Apps list: per application and per version stability" entry of this release. -
DEX statistics core: the server gains one tested home (
dex_stats.hpp) for the statistics the DEX application views will print — Wilson intervals for proportions, exact Poisson intervals for rates over exposure, the rule of three for zero counts, an exact conditional rate-ratio interval with a minimum-exposure guard, a small-count reliability flag, and a cohort-floor helper that withholds the statistic belowkDexCohortFloordevices (one device: only an empty cohort is withheld), reporting only the device count and a below-floor flag; intervals are 95 percent two-sided except the one-sided rule of three; rates exact up to one million events with closed-form bounds beyond; rate ratios with the exact conditional interval, to within 2e-5 relative, for arms up to one billion events (none above); no page or API changes in this entry (the DEX app performance and Apps list entries in this release print these statistics). -
Instruction discovery now publishes the schema the server enforces.
discover_instructions(MCP) andGET /api/v1/discover/instructionscarryinput_schemaper definition: the canonical JSON Schema thatPOST /api/instructions/{id}/executeenforces (ornull), alongsideinput_schema_error(null, `parameter_schema_not_ca...
Yuzu v0.14.1
Added
-
The agent now says whether it is verifying OTA update signatures. At startup it logs its update-signature mode and the trust bundle path —
OTA update signature mode: off(no bundle, so update binaries are not signature-checked),bundle, orbundle+require. When auto-update is enabled, the OTA update checker also warns, once, when it first starts (after the agent first connects) if the bundle is set but cannot be loaded. Every heartbeat carries the same mode as the status tagyuzu.ota_signature_mode; the tag is stored by the server, not yet surfaced in any view — the startup log line is the per-endpoint source of truth. The agent also warns at startup about any environment variable starting withYUZU_UPDATE_that it does not read, so a misspelling AFTER theYUZU_UPDATE_prefix (for exampleYUZU_UPDATE_TRUST_BUNDEL) no longer leaves signing silently off. The WindowsEnvironment-value check script in the server administration guide now also rejects non-ASCII characters in the signing entries and the binary path, and requires$stageto be 1 or 2. (#5249) -
--postgres-dsn-fileand--oidc-client-secret-fileread the server's database connection string and OIDC client secret from a file, so they need not appear on the command line, where other local users can read them. On Windows, the service's environment is readable by local users too. The whole file is the value; trailing whitespace and a UTF-8 byte-order mark are ignored. The server refuses to start if the file is missing, empty or larger than 64 KiB, or if the matching direct option (--postgres-dsn/YUZU_POSTGRES_DSN,--oidc-client-secret/YUZU_OIDC_CLIENT_SECRET) is also set. This works with the usual Docker and Kubernetes secret files. Env:YUZU_POSTGRES_DSN_FILE,YUZU_OIDC_CLIENT_SECRET_FILE. (#5272)
Changed
-
Breaking for alert rules — an OTA update refused because the agent's own trust bundle cannot be loaded is now counted as
yuzu_agent_ota_signature_refused_total{reason="bundle_unreadable"}and logged asOTA update <version> REFUSED: the update trust bundle could not be loaded, no longerreason="untrusted"/signature verification FAILED (untrusted chain)(the grep string the 0.14.0 notes gave for #5196). Rules, dashboards or saved searches keyed on the old reason or text for this fault need updating; for those faults (missing, permission-denied, not PEM) the refusal itself is unchanged, and the heartbeat totalyuzu.ota_signature_refused(soyuzu_fleet_ota_signature_refusing_agents) still includes it. New refusal: both--update-trust-bundleand--plugin-trust-bundleare now read bounded — a bundle over 1 MiB, one that is not a regular file, or (Windows) one held open by another process that does not allow shared reading is refused, where 0.14.0 loaded it; with--update-require-signatureor--plugin-require-signaturethat blocks every signed update or plugin, so check the bundle size before upgrading (see the upgrade notes). Agent-only; no server change. (#5249) -
The release workflow now refuses to publish over a tag that already has a published release (#5282). A new first job in
release.yml,release-guard, refuses any run that is not on avX.Y.Z[-{alpha,beta,rc}N]tag, and fails the run before any build or push whenGET /releases/tags/<tag>answers 200; it proceeds only on HTTP 404 and fails closed on any other answer. Every image push and the release creation additionally re-check that the tag still points at the run's commit (scripts/ci/check-release-tag-sha.sh). Every image-publishing job, the release job and the build jobs wait for it. Before this, a run started against an already-released tag (by mistake, or a superseded run) pushed new image digests over:X.Y.Z(and:X.Y/:latest) that no longer matched the release's signedSHA256SUMSand SBOMs, andCreate GitHub Releasethen skipped with a notice and went green; that step now fails instead. Recovery for a failed job is "Re-run failed jobs" on the newest run for the tag (digests kept); there is no override, and redoing a published release means cutting a new version. Residual paths are listed indocs/ci-architecture.md, Release re-publish guard.
Fixed
-
The Windows agent installer no longer leaves a running agent stopped when an upgrade fails. If an upgrade fails after the installer has stopped the
YuzuAgentservice (for example, a file in the program directory cannot be replaced, which an unattended/SUPPRESSMSGBOXESinstall answers with Abort, exit code 5), the installer now starts the service again before it exits if it was running or still starting, even when/NOSTARTwas given. It waits for a slow stop to finish and then records in the Setup log (/LOG=) the state it actually saw: RUNNING, or that the service could NOT be started, withsc.exe's exit code. Files the failed upgrade had already replaced are not put back, so run the installer again. Earlier installers left the agent offline until someone ransc.exe start YuzuAgentor rebooted. A trust-anchor refusal (exit code 7) still happens before the service is stopped. Its message now also says what the attempt left at%ProgramData%\Yuzu\agent-certs: when the lock on a new directory is blocked, nothing is created there, and an existing directory's own permissions are never changed. The agent and server installers' messages now saysc.exe, never plainsc, which PowerShell reads as Set-Content. (#5250) -
A docs- or changelog-only push to
main/devnow gets the required CI checks (#5320).ci.yml's push trigger dropped itspathsfilter, so every push runs; preflight'scode_changedgate skips the build matrix for a docs-only push, as it already did for a docs-only PR, and the required contexts appear on the commit as success or skipped instead of being absent. The v0.14.0 release commit was tagged without them because its only change wasCHANGELOG.md. The build legs are skipped only when the previous commit's push run succeeded, the push is a fast-forward, and every changed path (and every rename's previous path) is in the docs set — so the skip never follows an unbuilt or red code commit; any API error, control character in a path, or other answer builds. A push touchingdocs/os-capability-matrix.mdstill builds and runs its drift gate. The release skill's tag gate now says that skipped required checks count as passing and absent, failed or cancelled ones do not. -
Action required before upgrading from 0.14.0 on Docker Compose: copy
/etc/yuzu/certsout of the running server container first (Upgrading → "Docker Compose: copy/etc/yuzu/certsout of the server container before you recreate it"). Affected: a 0.14.0 Docker Compose stack whose server has no named volume on/etc/yuzu/certswhile Postgres has one —deploy/docker/docker-compose.yml(the 0.14.0 README quickstart),docker-compose.uat.yml,docker-compose.full-uat.yml,docker-compose.viz-uat.yml,docker-compose.demo.ymlused with--keep, any compose copied from them, and Compose Wizard output from before 0.14.1 in Plaintext mode, with 'Persist generated certs' unticked, or with named volumes off and an external Postgres. Not affected:docker-compose.reference.yml(which the README quickstart saves asdocker-compose.ymlsince #5419) anddocker-compose.reference-gateway.yml(theircertsvolume). The server keeps its internal CA, default certificates and secrets key-encryption key (KEK) in/etc/yuzu/certs, and since 0.14.0 the KEK is also registered in Postgres. In those stacks an image upgrade,docker compose downthenup, or--force-recreatedeleted the key files, and the server then refused to start withkek_unresolvable. Those composes and the/testupgrade-test compose now mount aserver-certsvolume on/etc/yuzu/certs. The Compose Wizard's Default and Plaintext output always mount a volume there too (its "Persist generated certs" option is now always on): the namedserver-certsvolume when named volumes are on or Postgres is external, otherwise an anonymous volume that is discarded together with the database's anonymous volume. The wizard's Operator-certs mode still mounts./certsread-only, so the server cannot write its KEK and stops at first boot withprovider_failure(#5420). The path does not move, so CA key paths recorded in Postgres stay valid. The chiselled server image now creates/etc/yuzu/certsowned by the server user, and the demo stack adds a one-shotserver-certs-initservice so it also works on older chiselled images. The upgrade guide gives a checked copy-out recipe, the diagnostics to run before anything destructive (a lost volume line, the wrong file owner, or keys left on a dangling anonymous volume give the same error), a rollback that keeps the keys, and what can still be saved if the files are already gone. The/testupgrade gate also recreates the upgraded server and checks that it comes back on the same keys. (#5370) -
The README's Docker Compose quickstart now works with the released images. In 0.14.0 it downloaded
deploy/docker/docker-compose.ymland ranYUZU_VERSION=0.14.0 docker compose up -d. That compose builds from a checkout of the repository (build: context: ../..), so a downloaded copy has nothing to build,YUZU_VERSIONwas ignored, and the stack never started. The quickstart now downloadsdeploy/docker/docker-compose.reference.ymlfrom the release tag, which pullsghcr.io/devnullltd/yuzu-serverandyuzu-postgresat${YUZU_VERSION}, serves HTTPS on 8443 and keeps/etc/yuzu/certson a named volume. It is three short steps that paste safely into bash or zsh: create the directory and a.envwith the version and two different Postgres passwords, write the first admin account toyuzu-server.cfg(the server's interactive first-run setup exits when there is no terminal), then start the stack and check `/read...
Yuzu v0.14.0
⚠️ Docker Compose users: copy/etc/yuzu/certsout of the server container before you next recreate it. In 0.14.0,deploy/docker/docker-compose.yml(the README quickstart),docker-compose.uat.yml,docker-compose.full-uat.yml,docker-compose.viz-uat.yml,docker-compose.demo.yml(with--keep), and Compose Wizard output in Plaintext mode, with "Persist generated certs" unticked, or with named volumes off and an external Postgres, keep the server's CA and secrets key-encryption key inside the container. Recreating the container deletes them: that covers an image upgrade,docker compose downthenup, andup --force-recreate. The server then refuses to start (kek_unresolvable), and anything encrypted with that key, MFA secrets included, cannot be recovered. Don't recreate the server container, and don't rundocker compose down -v, until you have followed the copy-out recipe indocs/user-manual/upgrading.md, section "Docker Compose: copy/etc/yuzu/certsout of the server container before you recreate it". The compose files are fixed onmainand in 0.14.1 (#5370). Not affected:docker-compose.reference.yml,docker-compose.reference-gateway.yml, and non-Docker installs.
The full 0.14.0 changelog is longer than a GitHub release can hold. The list below is cut short; the complete
## [0.14.0]section is inCHANGELOG.md.
Before you upgrade
- Windows agent: set update-signing options in the service's
Environmentregistry value, not its binary path. Every run of the agent installer, including an upgrade pushed by SCCM, Intune or GPO, rewrites theYuzuAgentservice's binary path and silently drops--update-trust-bundleand--update-require-signaturefrom it. OTA updates are then no longer signature-checked. Windows: the service'sEnvironmentvalue indocs/user-manual/server-admin.mdgives the commands and a check. (#5196) - Windows agent installer: pilot it first. It now refuses, with exit code 7 and the reason in its
/LOG=file, an%ProgramData%\Yuzu\agent-certsfolder it did not secure, or one holding anything other than plain files owned by Administrators or SYSTEM. If it refuses, the agent service is left running. If an endpoint ran a 0.14.0 release candidate from rc1 to rc5, see OTA update binary signing in the user manual to repairagent-certs. 0.13.0 endpoints are not affected. (#5196) - Linux native packages need a recent distribution. See the known issue below. The container images are the supported route on older hosts. (#5143)
Known issues
Windows server installer (#5196, #5210)
The Windows server installer adds permissions to %ProgramData%\Yuzu Server but does not remove the ones it inherits, so local users on that machine can read its contents. That includes yuzu-server.cfg (password hashes) and any TLS key copied into certs\. A silent install's /LOG= file also contains the installer's full command line, including any passwords passed to it: protect or delete it. Windows server is not a supported deployment (ADR-0035), and a service registered by this installer cannot start without a manually configured Postgres connection (#5272). If you run it anyway, restrict that folder to Administrators and SYSTEM, and do not let untrusted users sign in to the machine. A redesigned installer is planned for 0.14.1.
Linux native packages need a recent distribution (#5143, #5171)
The .deb, .rpm and tarball binaries need glibc 2.38. The server also needs GCC 15's libstdc++, and the agent GCC 14's. In practice the server runs on Ubuntu 26.04 and Fedora 42, and the agent also runs on Ubuntu 24.04. None of them runs on Ubuntu 22.04, Debian 12 or RHEL/Rocky 9: the server and agent .rpm refuse to install there, while the .deb packages install but do not start. The gateway also needs OpenSSL 3, and its .rpm does not start on Fedora 42. The container images are not affected (except that the Linux agent delivered by yuzu-agent-bundle-chisel has the same requirements); use them for the server and gateway elsewhere. Building to an older baseline is planned for 0.14.1.
Added
-
ADR-0031 operator surface: server wiring, MCP twins, and the authenticated
upload protocol replace the legacy file-retrieval stub (PR1.5c + PR1.6c).
PluginConfigStoreandUploadGrantStore(PR1.5b/PR1.6a) are now constructed
at server startup and wired to their REST routes
(/api/v1/plugin-config/*,/api/v1/upload-grants*) and to eight new MCP
tools (get/list/set/delete_plugin_config,set/delete_plugin_secret,
get/set_plugin_kill_switch), alongside the three upload-grant MCP twins
(mint/list/revoke_upload_grant) already shipped in #3135 — all eleven
carry the same securable, operation, and audit verb as their REST twin. The
agent-authenticated upload session endpoints (POST /api/v1/uploads,PUT .../chunk,GET .../{upload_id},POST .../commit,DELETE .../{upload_id}) are documented in the OpenAPI spec but deliberately have
no MCP twin — they authenticate on a grant/session bearer credential, never
an operator session. The legacy, unauthenticatedPOST /api/v1/file-retrievalstub (accepted a body-suppliedagent_id, stored
nothing) is removed; exactly one handler now serves file retrieval.
ScheduleRunner's arming re-check (PR1.5a) is wired to a real
authorization lookup: a schedule whose creator has since lost the required
permission for its targetplugin.actionno longer fires, on both the
auto and approval-gated paths.PluginConfigStore's secret column is now
enrolled in the live KEK rotation/rewrap/status surface alongside the auth
store's — an operator's/secrets/kek/rotateor/rewrapcall re-wraps
plugin secrets too, closing the gapdocs/adr/3005-plugin-config-store.md
recorded as open. (PR1.5c/PR1.6c) -
Plugin config/secret plane + per-action kill switch. Born-on-Postgres
PluginConfigStore
(schemaplugin_config_store) gives later default-off plugins a place to keep operator
configuration, envelope-encrypted secrets (SecretCodec, enrolled in the live KEK-rotation
surface), and a per-plugin or per-action kill switch. REST surface at
/api/v1/plugin-config/*: list/get/set/delete config (PluginConfig:Read/Write/Delete),
set/delete secret (PluginSecret:Write/Delete, write-only — no route ever returns a secret's
plaintext), get/set kill switch (PluginConfig:Read/Write). Kill-switch evaluation is
fail-closed: a degraded store reads as "disabled", never "enabled". Agent-side secret delivery
is a deliberately open item — seedocs/adr/3005-plugin-config-store.md. (PR1.5b) -
Typed schedule parameters + arming re-check on every fire path
(PR1.5a).POST /api/schedulesnow accepts an optionalparameters
object (string/number/boolean values only; nested objects/arrays, an
empty/underscore-prefixed key, more than 32 keys, or a canonical
serialization over 4KB are rejected with a400). The stored form is
canonical — keys sorted, so the same logical parameters always serialize
identically regardless of the order the caller wrote them in — and reaches
the scheduled dispatch exactly as a real
std::unordered_map<std::string,std::string>, replacing the hardcoded
empty"{}"ScheduleRunnerpreviously sent on every fire. Parameters are
set at creation only; there is no update route (delete and recreate to
change them). Existing schedules migrate forward with the canonical empty
object and are unaffected.
Separately,ScheduleRunner::fire()now re-verifies the arming principal's
authority (Deps::arming_check) BEFORE branching on approval, so the
re-check covers both the direct-dispatch (approval_mode=="auto") path
and the approval-gated path — not only the latter. The callback is
fail-closed: unset, it denies the fire, audits the denial (principal +
targetplugin.action), counts it, and still advances the schedule so a
permanently-denied schedule cannot spin. It sits in front of the existing
approval-ticket flow (ApprovalManager/fire_with_approval) as an
additional gate, not a replacement for it. -
One-time upload grant + authenticated chunked artifact receive (CC-06 fix, server side). The
server gains a new/api/v1/upload-grants(mint/list/revoke, operator-gated) +/api/v1/uploads
(session open, chunked PUT, status/resume, commit, cancel) REST surface backed by a new
born-PostgresUploadGrantStore, replacing the trust model of the old
POST /api/v1/file-retrievalstub — which accepted a client-assertedagent_idand destination
path with no real authentication and never durably wrote the uploaded bytes. An operator mints a
single-use grant naming the agent/source/size/hash bounds; redeeming it (atomically, exactly
once) opens an upload session whose own credential authenticates every chunk. Neither credential
is ever stored in plaintext (SHA-256 digest only, one-time reveal at mint/session-open). See
docs/adr/3004-artifact-blob-storage.mdfor the design. -
Cross-fragment capability-catalogue drift gate (PR1.9). Two new hermetic tests keep the five per-plugin-group
capability_decls/plugin_action_catalogue_*.hppfragments pluscore_dispatch_capabilities.hpphonest as a whole, since no single fragment's author can see the other five.tests/test_capability_catalogue_complete.py(python3 tests/test_capability_catalogue_complete.py) parses every plugin'sactions()override and cross-references it against all six catalogue sources, naming the exactplugin.actionwhen a plugin declares an action with no catalogue row, a fragment declares a row no plugin actually has, or the sameplugin.actionis declared by more than one source. `tests/unit/server/test_...
Yuzu v0.14.0-rc7
Changes since v0.14.0-rc6
- Every image SBOM is now inside the signed
SHA256SUMS. The release job now waits for the three chisel image builds and refuses to publish if any expected SBOM is missing. An image build whose SBOM upload fails now fails instead of passing. In rc2, rc4 and rc6 some chisel SBOMs were missing or attached afterwards. This release carries all of them:yuzu-{server,gateway,agent}-chisel-image.{cdx,spdx}.json, alongside the server, gateway and postgres image SBOMs. (#5242, #5283) - Agent: a command stopped at its line limit no longer reports its output as truncated. When the agent stopped a command at its configured line count, output the command had already written was still counted afterwards and could trip the 1 MB truncation flag. The only effect was a spurious truncation warning. The returned lines were always correct. (#5298, #5311)
- Documentation: the Linux platform floor is now stated in the user manual (
docs/user-manual/README.md), with the distributions each component runs on and the ones it does not. (#5143, #5277) - Release tooling:
assemble-changelog.py promote X.Y.Z --appendfolds hotfix changelog fragments that land after a version's first release candidate into its existing section. The CHANGELOG's0.14.0section now includes every hotfix since rc1. (#5221, #5284, #5297)
Known issue: 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.
Known issue: Linux native packages need a recent distribution (#5143, #5171)
The .deb, .rpm and tarball binaries need glibc 2.38. The server also needs GCC 15's libstdc++, and the agent GCC 14's. In practice the server runs on Ubuntu 26.04 and Fedora 42, and the agent also runs on Ubuntu 24.04. None of them runs on Ubuntu 22.04, Debian 12 or RHEL/Rocky 9: the server and agent .rpm refuse to install there, while the .deb packages install but do not start. The gateway also needs OpenSSL 3, and its .rpm does not start on Fedora 42. The container images are not affected (except that the Linux agent delivered by yuzu-agent-bundle-chisel has the same requirements); use them for the server and gateway elsewhere. Building to an older baseline is planned for 0.14.1.
Added
-
ADR-0031 operator surface: server wiring, MCP twins, and the authenticated
upload protocol replace the legacy file-retrieval stub (PR1.5c + PR1.6c).
PluginConfigStoreandUploadGrantStore(PR1.5b/PR1.6a) are now constructed
at server startup and wired to their REST routes
(/api/v1/plugin-config/*,/api/v1/upload-grants*) and to eight new MCP
tools (get/list/set/delete_plugin_config,set/delete_plugin_secret,
get/set_plugin_kill_switch), alongside the three upload-grant MCP twins
(mint/list/revoke_upload_grant) already shipped in #3135 — all eleven
carry the same securable, operation, and audit verb as their REST twin. The
agent-authenticated upload session endpoints (POST /api/v1/uploads,PUT .../chunk,GET .../{upload_id},POST .../commit,DELETE .../{upload_id}) are documented in the OpenAPI spec but deliberately have
no MCP twin — they authenticate on a grant/session bearer credential, never
an operator session. The legacy, unauthenticatedPOST /api/v1/file-retrievalstub (accepted a body-suppliedagent_id, stored
nothing) is removed; exactly one handler now serves file retrieval.
ScheduleRunner's arming re-check (PR1.5a) is wired to a real
authorization lookup: a schedule whose creator has since lost the required
permission for its targetplugin.actionno longer fires, on both the
auto and approval-gated paths.PluginConfigStore's secret column is now
enrolled in the live KEK rotation/rewrap/status surface alongside the auth
store's — an operator's/secrets/kek/rotateor/rewrapcall re-wraps
plugin secrets too, closing the gapdocs/adr/3005-plugin-config-store.md
recorded as open. (PR1.5c/PR1.6c) -
Plugin config/secret plane + per-action kill switch. Born-on-Postgres
PluginConfigStore
(schemaplugin_config_store) gives later default-off plugins a place to keep operator
configuration, envelope-encrypted secrets (SecretCodec, enrolled in the live KEK-rotation
surface), and a per-plugin or per-action kill switch. REST surface at
/api/v1/plugin-config/*: list/get/set/delete config (PluginConfig:Read/Write/Delete),
set/delete secret (PluginSecret:Write/Delete, write-only — no route ever returns a secret's
plaintext), get/set kill switch (PluginConfig:Read/Write). Kill-switch evaluation is
fail-closed: a degraded store reads as "disabled", never "enabled". Agent-side secret delivery
is a deliberately open item — seedocs/adr/3005-plugin-config-store.md. (PR1.5b) -
Typed schedule parameters + arming re-check on every fire path
(PR1.5a).POST /api/schedulesnow accepts an optionalparameters
object (string/number/boolean values only; nested objects/arrays, an
empty/underscore-prefixed key, more than 32 keys, or a canonical
serialization over 4KB are rejected with a400). The stored form is
canonical — keys sorted, so the same logical parameters always serialize
identically regardless of the order the caller wrote them in — and reaches
the scheduled dispatch exactly as a real
std::unordered_map<std::string,std::string>, replacing the hardcoded
empty"{}"ScheduleRunnerpreviously sent on every fire. Parameters are
set at creation only; there is no update route (delete and recreate to
change them). Existing schedules migrate forward with the canonical empty
object and are unaffected.
Separately,ScheduleRunner::fire()now re-verifies the arming principal's
authority (Deps::arming_check) BEFORE branching on approval, so the
re-check covers both the direct-dispatch (approval_mode=="auto") path
and the approval-gated path — not only the latter. The callback is
fail-closed: unset, it denies the fire, audits the denial (principal +
targetplugin.action), counts it, and still advances the schedule so a
permanently-denied schedule cannot spin. It sits in front of the existing
approval-ticket flow (ApprovalManager/fire_with_approval) as an
additional gate, not a replacement for it. -
One-time upload grant + authenticated chunked artifact receive (CC-06 fix, server side). The
server gains a new/api/v1/upload-grants(mint/list/revoke, operator-gated) +/api/v1/uploads
(session open, chunked PUT, status/resume, commit, cancel) REST surface backed by a new
born-PostgresUploadGrantStore, replacing the trust model of the old
POST /api/v1/file-retrievalstub — which accepted a client-assertedagent_idand destination
path with no real authentication and never durably wrote the uploaded bytes. An operator mints a
single-use grant naming the agent/source/size/hash bounds; redeeming it (atomically, exactly
once) opens an upload session whose own credential authenticates every chunk. Neither credential
is ever stored in plaintext (SHA-256 digest only, one-time reveal at mint/session-open). See
docs/adr/3004-artifact-blob-storage.mdfor the design. -
Cross-fragment capability-catalogue drift gate (PR1.9). Two new hermetic tests keep the five per-plugin-group
capability_decls/plugin_action_catalogue_*.hppfragments pluscore_dispatch_capabilities.hpphonest as a whole, since no single fragment's author can see the other five.tests/test_capability_catalogue_complete.py(python3 tests/test_capability_catalogue_complete.py) parses every plugin'sactions()override and cross-references it against all six catalogue sources, naming the exactplugin.actionwhen a plugin declares an action with no catalogue row, a fragment declares a row no plugin actually has, or the sameplugin.actionis declared by more than one source.tests/unit/server/test_capability_catalogue.cpp([server][dispatch][capability]) composes aCommandCapabilityRegistryover all six sources and asserts the catalogue-wide invariants: every row'srisk_tieris at or above its operation's floor, everysecurable/operationis onerbac_store.cppactually seeds, everyDestructiverow isIrreversible,system_reservedis true only forcore_dispatch_capabilities.hpprows,classify()resolves every declared pair, and a locally-constructed duplicate span makes the registry reportAmbiguous. -
Command capability classification core (PR1.9a). New header-only
server/core/src/command_capability.hpp/command_capability_parsers.hppdefine theplugin.actionclassification vocabulary (DispatchClass,Mutability) and aCommandCapabilityRegistrythat composes several independently-authored capability fragments and classifies a dispatch case-insensitively — an unknownplugin.actionis alwaysUnclassified, never a permissive default, and the sameplugin.actiondeclared by two fragments isAmbiguous, never first-wins. Also ships the frozenv1|<class>|<mutability>|<plan_hash_hex>dispatch-tag wire grammar (encode/decode) and an order-invariantcompute_plan_hash.capability_decls/core_dispatch_capabilities.hppdeclares the three system-initiated dispatc...
Yuzu v0.14.0-rc6
Note (added after publication):
yuzu-agent-chisel-image.cdx.json/.spdx.jsonandyuzu-server-chisel-image.cdx.json/.spdx.jsonwere missing from this release because of a job-ordering race in the release workflow (#5242). They were attached later from this release's own build run, and are not listed in this release's signedSHA256SUMS. Each names the image it describes, and each matches the published0.14.0-rc6image:ghcr.io/devnullltd/yuzu-agent-chisel@sha256:4d0075315e6103381da58d431fee935b36d8a6e53bb30c4da6a228fd7bfc9d58andghcr.io/devnullltd/yuzu-server-chisel@sha256:d5e0b248b7498adef32328647d113065d784b2848dd47aa956c065b7f0db9fc1.
Changes since v0.14.0-rc5
Windows agent installer hotfix (#5255, refs #5196):
-
Reinstalling or upgrading the Windows agent no longer locks the agent out of its update trust bundle. Agent installers from 0.14.0-rc1 to rc5 gave every file already in
%ProgramData%\Yuzu\agent-certs, such as an operator-provisionedupdate-trust-bundle.pem, an empty permission list, and still reported success. SYSTEM, and so the agent, could then no longer read the bundle, so every OTA update was refused (yuzu_agent_ota_signature_refused_totalrises, and the agent logssignature verification FAILED (untrusted chain)). 0.13.0 is not affected. To repair an affected endpoint, run, elevated:icacls "C:\ProgramData\Yuzu\agent-certs\*" /reset /L /C /QThis resets the files inside the folder, never the folder itself, whose own permissions must stay as they are; run it only after checking that the folder holds nothing but files and that each is owned by Administrators or SYSTEM. Or run the procedure block in OTA update binary signing (
docs/user-manual/server-admin.md), which does those checks, repairs the files and verifies the result; it also replaces that page's earlier manual procedure, which had the same mistake. If an endpoint ran one of these release candidates and you did not place itsupdate-trust-bundle.pemyourself, delete the file and copy in a fresh one: those installers took over whatever was already in the folder. Installing this version repairs the folder too, but any installer run rewrites the service's binary path, so read the security note about update-signing flags first. The installer's check now compares the permissions exactly, so a file nobody can read, a deny entry or an inherit-only entry stops the install instead of passing. The installer also no longer takes over a folder or file it did not create or an administrator did not place. Ifagent-certsalready exists it must already be secured (as earlier installers left it), each file in it must be owned by Administrators or SYSTEM, and the folder may hold only plain files; otherwise the install stops, giving the reason in its/LOG=file, before the agent service is stopped. Nothing it runs on the folder is recursive any more. Provision the bundle by copying it in as an administrator after the agent is installed. Setup also no longer fails when the installing account's profile path contains a typographic apostrophe. Found on Windows Server 2022. (#5196) -
The Windows agent installer now runs where PowerShell is restricted to Constrained Language Mode. That mode is imposed by WDAC script enforcement, and by AppLocker script rules for an install run by an administrator rather than as SYSTEM. It refused the .NET method calls the installer's trust-anchor permission check used, so the check could not complete, and agent installers from 0.14.0-rc1 to rc5 stopped there with exit code 7. The check now reads and compares the security descriptor text, which Constrained Language Mode allows. On Windows Server 2022, with AppLocker script rules enforced and the installer run by an elevated administrator, the rc5 installer stopped with exit code 7 and this one completed. (#5196)
-
On Windows, set the agent's update-signing options in the service's
Environmentregistry value, not in its binary path. Every run of the agent installer, including a reinstall or an upgrade pushed through SCCM, Intune or GPO, rewrites theYuzuAgentservice's binary path in full. If you added--update-trust-bundleand--update-require-signaturethere, it drops them without reporting anything, and OTA updates are no longer signature-checked. The service'sEnvironmentvalue (HKLM\SYSTEM\CurrentControlSet\Services\YuzuAgent,REG_MULTI_SZ) is read each time the service starts, and installing over the existing agent leaves it in place. Uninstalling the agent deletes it, so set it again after any uninstall, including a deployment that uninstalls the previous version first. Windows: the service'sEnvironmentvalue indocs/user-manual/server-admin.mdgives the commands, the mistakes that fail silently, and a check that exits non-zero when an endpoint is not configured. Run it on endpoints where you enabled signing. (#5196)
Before rolling out the Windows agent installer: pilot it on a few endpoints first. It now refuses (exit code 7, reason in its /LOG= file, agent service left running) an agent-certs folder it did not secure, or one holding anything other than plain files owned by Administrators or SYSTEM. See Upgrading (0.14.x) and OTA update binary signing in the user manual.
Follow-ups: #5249, #5250, #5257, #5258, #5263.
Known issue: 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.
Known issue: Linux native packages need a recent distribution (#5143, #5171)
The .deb, .rpm and tarball binaries need glibc 2.38. The server also needs GCC 15's libstdc++, and the agent GCC 14's. In practice the server runs on Ubuntu 26.04 and Fedora 42, and the agent also runs on Ubuntu 24.04. None of them runs on Ubuntu 22.04, Debian 12 or RHEL/Rocky 9: the server and agent .rpm refuse to install there, while the .deb packages install but do not start. The gateway also needs OpenSSL 3, and its .rpm does not start on Fedora 42. The container images are not affected (except that the Linux agent delivered by yuzu-agent-bundle-chisel has the same requirements); use them for the server and gateway elsewhere. Building to an older baseline is planned for 0.14.1.
Added
-
ADR-0031 operator surface: server wiring, MCP twins, and the authenticated
upload protocol replace the legacy file-retrieval stub (PR1.5c + PR1.6c).
PluginConfigStoreandUploadGrantStore(PR1.5b/PR1.6a) are now constructed
at server startup and wired to their REST routes
(/api/v1/plugin-config/*,/api/v1/upload-grants*) and to eight new MCP
tools (get/list/set/delete_plugin_config,set/delete_plugin_secret,
get/set_plugin_kill_switch), alongside the three upload-grant MCP twins
(mint/list/revoke_upload_grant) already shipped in #3135 — all eleven
carry the same securable, operation, and audit verb as their REST twin. The
agent-authenticated upload session endpoints (POST /api/v1/uploads,PUT .../chunk,GET .../{upload_id},POST .../commit,DELETE .../{upload_id}) are documented in the OpenAPI spec but deliberately have
no MCP twin — they authenticate on a grant/session bearer credential, never
an operator session. The legacy, unauthenticatedPOST /api/v1/file-retrievalstub (accepted a body-suppliedagent_id, stored
nothing) is removed; exactly one handler now serves file retrieval.
ScheduleRunner's arming re-check (PR1.5a) is wired to a real
authorization lookup: a schedule whose creator has since lost the required
permission for its targetplugin.actionno longer fires, on both the
auto and approval-gated paths.PluginConfigStore's secret column is now
enrolled in the live KEK rotation/rewrap/status surface alongside the auth
store's — an operator's/secrets/kek/rotateor/rewrapcall re-wraps
plugin secrets too, closing the gapdocs/adr/3005-plugin-config-store.md
recorded as open. (PR1.5c/PR1.6c) -
Plugin config/secret plane + per-action kill switch. Born-on-Postgres
PluginConfigStore
(schemaplugin_config_store) gives later default-off plugins a place to keep operator
configuration, envelope-encrypted secrets (SecretCodec, enrolled in the live KEK-rotation
surface), and a per-plugin or per-action kill switch. REST surface at
/api/v1/plugin-config/*: list/get/set/delete config (PluginConfig:Read/Write/Delete),
set/delete secret (PluginSecret:Write/Delete, write-only — no route ever returns a secret's
plaintext), get/set kill switch (PluginConfig:Read/Write). Kill-switch evaluation is
fail-closed: a degraded store reads as "disabled", never "enabled". Agent-side secret delivery
is a deliberately open item — seedocs/adr/3005-plugin-config-store.md. (PR1.5b) -
Typed schedule parameters + arming re-check on every fire path
(PR1.5a).POST /api/schedulesnow accepts an optionalparameters
object (string/number/boolean values only; nested objects/arrays, an
empty/underscore-prefixed key, more than 32 keys, or a canonical
serialization over 4KB are rejected with a400). The stored form is
canonical — keys sorted, so the same logical parameters always serialize
identically regardless of the order the caller w...
Yuzu v0.14.0-rc5
Changes since v0.14.0-rc4
These fixes are listed here because they land in the CHANGELOG's 0.14.0 section only at the final release.
- The gateway container starts on recent Intel CPUs again. On AMX-capable Intel hosts (Sapphire Rapids and newer, including AWS c7i, m7i and r7i instances), the
yuzu-gatewayimage crashed at startup withsys_sigaltstack(): Internal error: Failed to set alternate signal stackand kept restarting (docker psshowsRestarting (134)). The images from 0.13.0 (and its release candidates) through 0.14.0-rc4 are affected. They ran on Alpine 3.24 (musl 1.2.6), which rejects the signal stack size Erlang/OTP 28 asks for on those CPUs. The runtime is back on Alpine 3.23, matching the build, and the image build now fails if the two drift apart again. The chiselled gateway image uses glibc and was not affected. This fix has been verified by build and boot tests but has not yet been run on AMX hardware; that check (#5223) is a gate for 0.14.0 final. To check a host:grep -m1 -o amx_tile /proc/cpuinfo. Workarounds for older images are under "Gateway Disconnections" indocs/operations/troubleshooting.md. (#2150, #5225)
Warning
Windows server installs: restrict the data directory after installing. In every version so far, including this one, the server installer leaves %ProgramData%\Yuzu Server readable, and partly writable, by local users. That directory holds yuzu-server.cfg (password hashes) and the TLS private keys in certs\. Until this is fixed (#5196, #5210, both milestoned for 0.14.0), restrict the directory to Administrators and SYSTEM after installing. If other users had local access, rotate the TLS keys and change the admin and operator passwords.
Also not yet in the CHANGELOG (from v0.14.0-rc4)
- The gateway's Prometheus endpoint works again. From 0.14.0-rc1 to rc3, every scrape of the gateway's
:9568/metricsreturned HTTP 500. Five metric descriptions contained a non-ASCII dash that the Prometheus text formatter can't encode. As a result, every gateway metric and the wholeyuzu-gatewayalert group were blind, including the management-plane authentication alerts.YuzuGatewayDownalso fired while the gateway was healthy. Alert history from rc1 to rc3 is not evidence of gateway health. The management-plane rejection counter now has every reason series from startup, so the first rejection for a reason raisesYuzuGatewayMgmtAuthRejected. (#5177) - The Windows agent installer no longer aborts when started from PowerShell 7 through another process, such as a script calling the installer. In rc3 that failed with "the permissions could not be read". Every PowerShell step in the agent and server installers now uses Windows PowerShell's own modules. (#5176)
- The reference gateway compose template's agent loads its plugins again. If you copied
docker-compose.reference-gateway.yml, add--plugin-dir=/usr/lib/yuzu/pluginsto your agent service'scommand:. (#5134)
Also not yet in the CHANGELOG (from v0.14.0-rc3)
- Pre-release
.debdownloads verify againstSHA256SUMS. RC.debfiles are named with the tag's version; the package version is still0.14.0~rc5. (#5141) - The server and agent RPMs no longer fail with "nothing provides user(yuzu)". Two limits remain: the RPM services do not start yet (#4916), and the Linux binaries need glibc 2.38 or newer (#5143). (#5142)
- The macOS agent installed from the
.pkgstarts. (#5144) - A silent uninstall of the Windows agent or server finishes. The data directory is kept; it holds the agent's identity and mTLS private key. Uninstallers up to rc2 still hang, so upgrade before uninstalling silently. (#5147)
Added
-
ADR-0031 operator surface: server wiring, MCP twins, and the authenticated
upload protocol replace the legacy file-retrieval stub (PR1.5c + PR1.6c).
PluginConfigStoreandUploadGrantStore(PR1.5b/PR1.6a) are now constructed
at server startup and wired to their REST routes
(/api/v1/plugin-config/*,/api/v1/upload-grants*) and to eight new MCP
tools (get/list/set/delete_plugin_config,set/delete_plugin_secret,
get/set_plugin_kill_switch), alongside the three upload-grant MCP twins
(mint/list/revoke_upload_grant) already shipped in #3135 — all eleven
carry the same securable, operation, and audit verb as their REST twin. The
agent-authenticated upload session endpoints (POST /api/v1/uploads,PUT .../chunk,GET .../{upload_id},POST .../commit,DELETE .../{upload_id}) are documented in the OpenAPI spec but deliberately have
no MCP twin — they authenticate on a grant/session bearer credential, never
an operator session. The legacy, unauthenticatedPOST /api/v1/file-retrievalstub (accepted a body-suppliedagent_id, stored
nothing) is removed; exactly one handler now serves file retrieval.
ScheduleRunner's arming re-check (PR1.5a) is wired to a real
authorization lookup: a schedule whose creator has since lost the required
permission for its targetplugin.actionno longer fires, on both the
auto and approval-gated paths.PluginConfigStore's secret column is now
enrolled in the live KEK rotation/rewrap/status surface alongside the auth
store's — an operator's/secrets/kek/rotateor/rewrapcall re-wraps
plugin secrets too, closing the gapdocs/adr/3005-plugin-config-store.md
recorded as open. (PR1.5c/PR1.6c) -
Plugin config/secret plane + per-action kill switch. Born-on-Postgres
PluginConfigStore
(schemaplugin_config_store) gives later default-off plugins a place to keep operator
configuration, envelope-encrypted secrets (SecretCodec, enrolled in the live KEK-rotation
surface), and a per-plugin or per-action kill switch. REST surface at
/api/v1/plugin-config/*: list/get/set/delete config (PluginConfig:Read/Write/Delete),
set/delete secret (PluginSecret:Write/Delete, write-only — no route ever returns a secret's
plaintext), get/set kill switch (PluginConfig:Read/Write). Kill-switch evaluation is
fail-closed: a degraded store reads as "disabled", never "enabled". Agent-side secret delivery
is a deliberately open item — seedocs/adr/3005-plugin-config-store.md. (PR1.5b) -
Typed schedule parameters + arming re-check on every fire path
(PR1.5a).POST /api/schedulesnow accepts an optionalparameters
object (string/number/boolean values only; nested objects/arrays, an
empty/underscore-prefixed key, more than 32 keys, or a canonical
serialization over 4KB are rejected with a400). The stored form is
canonical — keys sorted, so the same logical parameters always serialize
identically regardless of the order the caller wrote them in — and reaches
the scheduled dispatch exactly as a real
std::unordered_map<std::string,std::string>, replacing the hardcoded
empty"{}"ScheduleRunnerpreviously sent on every fire. Parameters are
set at creation only; there is no update route (delete and recreate to
change them). Existing schedules migrate forward with the canonical empty
object and are unaffected.
Separately,ScheduleRunner::fire()now re-verifies the arming principal's
authority (Deps::arming_check) BEFORE branching on approval, so the
re-check covers both the direct-dispatch (approval_mode=="auto") path
and the approval-gated path — not only the latter. The callback is
fail-closed: unset, it denies the fire, audits the denial (principal +
targetplugin.action), counts it, and still advances the schedule so a
permanently-denied schedule cannot spin. It sits in front of the existing
approval-ticket flow (ApprovalManager/fire_with_approval) as an
additional gate, not a replacement for it. -
One-time upload grant + authenticated chunked artifact receive (CC-06 fix, server side). The
server gains a new/api/v1/upload-grants(mint/list/revoke, operator-gated) +/api/v1/uploads
(session open, chunked PUT, status/resume, commit, cancel) REST surface backed by a new
born-PostgresUploadGrantStore, replacing the trust model of the old
POST /api/v1/file-retrievalstub — which accepted a client-assertedagent_idand destination
path with no real authentication and never durably wrote the uploaded bytes. An operator mints a
single-use grant naming the agent/source/size/hash bounds; redeeming it (atomically, exactly
once) opens an upload session whose own credential authenticates every chunk. Neither credential
is ever stored in plaintext (SHA-256 digest only, one-time reveal at mint/session-open). See
docs/adr/3004-artifact-blob-storage.mdfor the design. -
Cross-fragment capability-catalogue drift gate (PR1.9). Two new hermetic tests keep the five per-plugin-group
capability_decls/plugin_action_catalogue_*.hppfragments pluscore_dispatch_capabilities.hpphonest as a whole, since no single fragment's author can see the other five.tests/test_capability_catalogue_complete.py(python3 tests/test_capability_catalogue_complete.py) parses every plugin'sactions()override and cross-references it against all six catalogue sources, naming the exactplugin.actionwhen a plugin declares an action with no catalogue row, a fragment declares a row no plugin actually has, or the sameplugin.actionis declared by more than one source.tests/unit/server/test_capability_catalogue.cpp([server][dispatch][capability]) composes aCommandCapabilityRegistryover all six sources and asserts the catalogue-wide invariants: every row'srisk_tieris at or above its operation's floor, everysecurable/operationis onerbac_store.cppactually seeds, everyDestructiverow isIrreversible,system_reservedis true only forcore_dispatch_capabilities.hpprows,classify()resolves every declared pair, and a locally-constructed duplicate span makes the registry reportAmbiguous. -
**Command...
Yuzu v0.14.0-rc4
Note (added after publication):
yuzu-agent-chisel-image.cdx.jsonand.spdx.jsonwere missing from this release because of a job-ordering race in the release workflow (#5242). They were attached later from this release's own build run. They are not listed in this release's signedSHA256SUMS. Each one names the image it describes,ghcr.io/devnullltd/yuzu-agent-chisel@sha256:c70d34adc05f4c9c71cac6efd47aef05eaba7f5ff8524bca2174b798b3ccff48, which is the published0.14.0-rc4image.
Changes since v0.14.0-rc3
These fixes are listed here because they land in the CHANGELOG's 0.14.0 section only at the final release.
- The gateway's Prometheus endpoint works again. From 0.14.0-rc1 to rc3, every scrape of the gateway's
:9568/metricsreturned HTTP 500. Five metric descriptions contained a non-ASCII dash that the Prometheus text formatter can't encode. As a result, every gateway metric and the wholeyuzu-gatewayalert group were blind, including the management-plane authentication alerts.YuzuGatewayDownalso fired while the gateway was healthy. Alert history from rc1 to rc3 is not evidence of gateway health. The management-plane rejection counter now has every reason series from startup, so the first rejection for a reason raisesYuzuGatewayMgmtAuthRejected. (#5177) - The Windows agent installer no longer aborts when started from PowerShell 7 through another process, such as a script calling the installer. In rc3 that failed with "the permissions could not be read". Every PowerShell step in the agent and server installers now uses Windows PowerShell's own modules. (#5176)
- The reference gateway compose template's agent loads its plugins again. If you copied
docker-compose.reference-gateway.yml, add--plugin-dir=/usr/lib/yuzu/pluginsto your agent service'scommand:. (#5134)
Warning
Windows server installs: restrict the data directory after installing. In every version so far, including this one, the server installer leaves %ProgramData%\Yuzu Server readable, and partly writable, by local users. That directory holds yuzu-server.cfg (password hashes) and the TLS private keys in certs\. Until this is fixed (#5196, #5210, both milestoned for 0.14.0), restrict the directory to Administrators and SYSTEM after installing. If other users had local access, rotate the TLS keys and change the admin and operator passwords.
Carried over from v0.14.0-rc3 (also not yet in the CHANGELOG)
- Pre-release
.debdownloads verify againstSHA256SUMS. RC.debfiles are named with the tag's version; the package version is still0.14.0~rc4. (#5141) - The server and agent RPMs no longer fail with "nothing provides user(yuzu)". Two limits remain: the RPM services do not start yet (#4916), and the Linux binaries need glibc 2.38 or newer (#5143). (#5142)
- The macOS agent installed from the
.pkgstarts. (#5144) - A silent uninstall of the Windows agent or server finishes. The data directory is kept; it holds the agent's identity and mTLS private key. Uninstallers up to rc2 still hang, so upgrade before uninstalling silently. (#5147)
Added
-
ADR-0031 operator surface: server wiring, MCP twins, and the authenticated
upload protocol replace the legacy file-retrieval stub (PR1.5c + PR1.6c).
PluginConfigStoreandUploadGrantStore(PR1.5b/PR1.6a) are now constructed
at server startup and wired to their REST routes
(/api/v1/plugin-config/*,/api/v1/upload-grants*) and to eight new MCP
tools (get/list/set/delete_plugin_config,set/delete_plugin_secret,
get/set_plugin_kill_switch), alongside the three upload-grant MCP twins
(mint/list/revoke_upload_grant) already shipped in #3135 — all eleven
carry the same securable, operation, and audit verb as their REST twin. The
agent-authenticated upload session endpoints (POST /api/v1/uploads,PUT .../chunk,GET .../{upload_id},POST .../commit,DELETE .../{upload_id}) are documented in the OpenAPI spec but deliberately have
no MCP twin — they authenticate on a grant/session bearer credential, never
an operator session. The legacy, unauthenticatedPOST /api/v1/file-retrievalstub (accepted a body-suppliedagent_id, stored
nothing) is removed; exactly one handler now serves file retrieval.
ScheduleRunner's arming re-check (PR1.5a) is wired to a real
authorization lookup: a schedule whose creator has since lost the required
permission for its targetplugin.actionno longer fires, on both the
auto and approval-gated paths.PluginConfigStore's secret column is now
enrolled in the live KEK rotation/rewrap/status surface alongside the auth
store's — an operator's/secrets/kek/rotateor/rewrapcall re-wraps
plugin secrets too, closing the gapdocs/adr/3005-plugin-config-store.md
recorded as open. (PR1.5c/PR1.6c) -
Plugin config/secret plane + per-action kill switch. Born-on-Postgres
PluginConfigStore
(schemaplugin_config_store) gives later default-off plugins a place to keep operator
configuration, envelope-encrypted secrets (SecretCodec, enrolled in the live KEK-rotation
surface), and a per-plugin or per-action kill switch. REST surface at
/api/v1/plugin-config/*: list/get/set/delete config (PluginConfig:Read/Write/Delete),
set/delete secret (PluginSecret:Write/Delete, write-only — no route ever returns a secret's
plaintext), get/set kill switch (PluginConfig:Read/Write). Kill-switch evaluation is
fail-closed: a degraded store reads as "disabled", never "enabled". Agent-side secret delivery
is a deliberately open item — seedocs/adr/3005-plugin-config-store.md. (PR1.5b) -
Typed schedule parameters + arming re-check on every fire path
(PR1.5a).POST /api/schedulesnow accepts an optionalparameters
object (string/number/boolean values only; nested objects/arrays, an
empty/underscore-prefixed key, more than 32 keys, or a canonical
serialization over 4KB are rejected with a400). The stored form is
canonical — keys sorted, so the same logical parameters always serialize
identically regardless of the order the caller wrote them in — and reaches
the scheduled dispatch exactly as a real
std::unordered_map<std::string,std::string>, replacing the hardcoded
empty"{}"ScheduleRunnerpreviously sent on every fire. Parameters are
set at creation only; there is no update route (delete and recreate to
change them). Existing schedules migrate forward with the canonical empty
object and are unaffected.
Separately,ScheduleRunner::fire()now re-verifies the arming principal's
authority (Deps::arming_check) BEFORE branching on approval, so the
re-check covers both the direct-dispatch (approval_mode=="auto") path
and the approval-gated path — not only the latter. The callback is
fail-closed: unset, it denies the fire, audits the denial (principal +
targetplugin.action), counts it, and still advances the schedule so a
permanently-denied schedule cannot spin. It sits in front of the existing
approval-ticket flow (ApprovalManager/fire_with_approval) as an
additional gate, not a replacement for it. -
One-time upload grant + authenticated chunked artifact receive (CC-06 fix, server side). The
server gains a new/api/v1/upload-grants(mint/list/revoke, operator-gated) +/api/v1/uploads
(session open, chunked PUT, status/resume, commit, cancel) REST surface backed by a new
born-PostgresUploadGrantStore, replacing the trust model of the old
POST /api/v1/file-retrievalstub — which accepted a client-assertedagent_idand destination
path with no real authentication and never durably wrote the uploaded bytes. An operator mints a
single-use grant naming the agent/source/size/hash bounds; redeeming it (atomically, exactly
once) opens an upload session whose own credential authenticates every chunk. Neither credential
is ever stored in plaintext (SHA-256 digest only, one-time reveal at mint/session-open). See
docs/adr/3004-artifact-blob-storage.mdfor the design. -
Cross-fragment capability-catalogue drift gate (PR1.9). Two new hermetic tests keep the five per-plugin-group
capability_decls/plugin_action_catalogue_*.hppfragments pluscore_dispatch_capabilities.hpphonest as a whole, since no single fragment's author can see the other five.tests/test_capability_catalogue_complete.py(python3 tests/test_capability_catalogue_complete.py) parses every plugin'sactions()override and cross-references it against all six catalogue sources, naming the exactplugin.actionwhen a plugin declares an action with no catalogue row, a fragment declares a row no plugin actually has, or the sameplugin.actionis declared by more than one source.tests/unit/server/test_capability_catalogue.cpp([server][dispatch][capability]) composes aCommandCapabilityRegistryover all six sources and asserts the catalogue-wide invariants: every row'srisk_tieris at or above its operation's floor, everysecurable/operationis onerbac_store.cppactually seeds, everyDestructiverow isIrreversible,system_reservedis true only forcore_dispatch_capabilities.hpprows,classify()resolves every declared pair, and a locally-constructed duplicate span makes the registry reportAmbiguous. -
Command capability classification core (PR1.9a). New header-only
server/core/src/command_capability.hpp/command_capability_parsers.hppdefine theplugin.actionclassification vocabulary (DispatchClass,Mutability) and aCommandCapabilityRegistrythat composes several independently-authored capability fragments and classifies a dispatch case-insensitively — an unknownplugin.actionis alwaysUnclassified, never a permissive default, and the sameplugin.actiondeclared by two fragments isAmbiguous, never first-wins. Also ships the frozen `v1||...
Yuzu v0.14.0-rc3
Changes since v0.14.0-rc2
Packaging fixes (#5160). These are listed here because they land in the CHANGELOG's 0.14.0 section at the final release.
- Pre-release
.debdownloads now verify againstSHA256SUMS. RC.debfiles are named with the tag's version (yuzu-agent_0.14.0-rc3_amd64.deb); the package's own version is still0.14.0~rc3. The release now fails before publishing if any asset name would be rewritten by GitHub, or if a package carries another release's version. v0.13.0-rc6 had shipped leftover rc4 packages. (#5141) - The server and agent RPMs no longer fail with "nothing provides user(yuzu)". Two limits remain: the RPM services do not start yet (#4916), and the Linux binaries need glibc 2.38 or newer (#5143). (#5142)
- The macOS agent installed from the
.pkgstarts again. The core library now sits beside the binary. (#5144) - A silent uninstall of the Windows agent or server now finishes. The data directory is kept, and it holds the agent's identity and mTLS private key. To decommission a device, revoke its certificate and delete the directory. Uninstallers up to rc2 still hang, so upgrade before uninstalling silently. (#5147)
Added
-
ADR-0031 operator surface: server wiring, MCP twins, and the authenticated
upload protocol replace the legacy file-retrieval stub (PR1.5c + PR1.6c).
PluginConfigStoreandUploadGrantStore(PR1.5b/PR1.6a) are now constructed
at server startup and wired to their REST routes
(/api/v1/plugin-config/*,/api/v1/upload-grants*) and to eight new MCP
tools (get/list/set/delete_plugin_config,set/delete_plugin_secret,
get/set_plugin_kill_switch), alongside the three upload-grant MCP twins
(mint/list/revoke_upload_grant) already shipped in #3135 — all eleven
carry the same securable, operation, and audit verb as their REST twin. The
agent-authenticated upload session endpoints (POST /api/v1/uploads,PUT .../chunk,GET .../{upload_id},POST .../commit,DELETE .../{upload_id}) are documented in the OpenAPI spec but deliberately have
no MCP twin — they authenticate on a grant/session bearer credential, never
an operator session. The legacy, unauthenticatedPOST /api/v1/file-retrievalstub (accepted a body-suppliedagent_id, stored
nothing) is removed; exactly one handler now serves file retrieval.
ScheduleRunner's arming re-check (PR1.5a) is wired to a real
authorization lookup: a schedule whose creator has since lost the required
permission for its targetplugin.actionno longer fires, on both the
auto and approval-gated paths.PluginConfigStore's secret column is now
enrolled in the live KEK rotation/rewrap/status surface alongside the auth
store's — an operator's/secrets/kek/rotateor/rewrapcall re-wraps
plugin secrets too, closing the gapdocs/adr/3005-plugin-config-store.md
recorded as open. (PR1.5c/PR1.6c) -
Plugin config/secret plane + per-action kill switch. Born-on-Postgres
PluginConfigStore
(schemaplugin_config_store) gives later default-off plugins a place to keep operator
configuration, envelope-encrypted secrets (SecretCodec, enrolled in the live KEK-rotation
surface), and a per-plugin or per-action kill switch. REST surface at
/api/v1/plugin-config/*: list/get/set/delete config (PluginConfig:Read/Write/Delete),
set/delete secret (PluginSecret:Write/Delete, write-only — no route ever returns a secret's
plaintext), get/set kill switch (PluginConfig:Read/Write). Kill-switch evaluation is
fail-closed: a degraded store reads as "disabled", never "enabled". Agent-side secret delivery
is a deliberately open item — seedocs/adr/3005-plugin-config-store.md. (PR1.5b) -
Typed schedule parameters + arming re-check on every fire path
(PR1.5a).POST /api/schedulesnow accepts an optionalparameters
object (string/number/boolean values only; nested objects/arrays, an
empty/underscore-prefixed key, more than 32 keys, or a canonical
serialization over 4KB are rejected with a400). The stored form is
canonical — keys sorted, so the same logical parameters always serialize
identically regardless of the order the caller wrote them in — and reaches
the scheduled dispatch exactly as a real
std::unordered_map<std::string,std::string>, replacing the hardcoded
empty"{}"ScheduleRunnerpreviously sent on every fire. Parameters are
set at creation only; there is no update route (delete and recreate to
change them). Existing schedules migrate forward with the canonical empty
object and are unaffected.
Separately,ScheduleRunner::fire()now re-verifies the arming principal's
authority (Deps::arming_check) BEFORE branching on approval, so the
re-check covers both the direct-dispatch (approval_mode=="auto") path
and the approval-gated path — not only the latter. The callback is
fail-closed: unset, it denies the fire, audits the denial (principal +
targetplugin.action), counts it, and still advances the schedule so a
permanently-denied schedule cannot spin. It sits in front of the existing
approval-ticket flow (ApprovalManager/fire_with_approval) as an
additional gate, not a replacement for it. -
One-time upload grant + authenticated chunked artifact receive (CC-06 fix, server side). The
server gains a new/api/v1/upload-grants(mint/list/revoke, operator-gated) +/api/v1/uploads
(session open, chunked PUT, status/resume, commit, cancel) REST surface backed by a new
born-PostgresUploadGrantStore, replacing the trust model of the old
POST /api/v1/file-retrievalstub — which accepted a client-assertedagent_idand destination
path with no real authentication and never durably wrote the uploaded bytes. An operator mints a
single-use grant naming the agent/source/size/hash bounds; redeeming it (atomically, exactly
once) opens an upload session whose own credential authenticates every chunk. Neither credential
is ever stored in plaintext (SHA-256 digest only, one-time reveal at mint/session-open). See
docs/adr/3004-artifact-blob-storage.mdfor the design. -
Cross-fragment capability-catalogue drift gate (PR1.9). Two new hermetic tests keep the five per-plugin-group
capability_decls/plugin_action_catalogue_*.hppfragments pluscore_dispatch_capabilities.hpphonest as a whole, since no single fragment's author can see the other five.tests/test_capability_catalogue_complete.py(python3 tests/test_capability_catalogue_complete.py) parses every plugin'sactions()override and cross-references it against all six catalogue sources, naming the exactplugin.actionwhen a plugin declares an action with no catalogue row, a fragment declares a row no plugin actually has, or the sameplugin.actionis declared by more than one source.tests/unit/server/test_capability_catalogue.cpp([server][dispatch][capability]) composes aCommandCapabilityRegistryover all six sources and asserts the catalogue-wide invariants: every row'srisk_tieris at or above its operation's floor, everysecurable/operationis onerbac_store.cppactually seeds, everyDestructiverow isIrreversible,system_reservedis true only forcore_dispatch_capabilities.hpprows,classify()resolves every declared pair, and a locally-constructed duplicate span makes the registry reportAmbiguous. -
Command capability classification core (PR1.9a). New header-only
server/core/src/command_capability.hpp/command_capability_parsers.hppdefine theplugin.actionclassification vocabulary (DispatchClass,Mutability) and aCommandCapabilityRegistrythat composes several independently-authored capability fragments and classifies a dispatch case-insensitively — an unknownplugin.actionis alwaysUnclassified, never a permissive default, and the sameplugin.actiondeclared by two fragments isAmbiguous, never first-wins. Also ships the frozenv1|<class>|<mutability>|<plan_hash_hex>dispatch-tag wire grammar (encode/decode) and an order-invariantcompute_plan_hash.capability_decls/core_dispatch_capabilities.hppdeclares the three system-initiated dispatches (tar.fleet_snapshot,__guard__.push_rules,asset_tags.sync). Not yet wired into a live dispatch path — a future PR consumes this registry from the dispatch chokepoint.authz_model.hppadditionally gains three newCapabilitySeedrows (PluginConfig,PluginSecret,UploadGrant), reusing only existingOperationenumerators.RbacStore::seed_defaults()seeds the same three securables intorbac.db— Administrator gets full CRUD via the existing loop, PlatformEngineer gets Read/Write/Delete on all three, and Operator gets Read onPluginConfig/UploadGrantonly, neverPluginSecret— closing the gap (peer finding PLAN-001) where a securable absent from the live seed cannot receive a default grant, denying even an Administrator on a fresh RBAC-enabled install. -
Dispatch classification tag on the wire, with a gateway wire-capability handshake (CC-03).
CommandRequestnow carriesdispatch_tag(field 9), the opaquev1|<class>|<mutability>|<plan_hash_hex>token the dispatch chokepoint uses to classify a command as read-only, mutating, or destructive. The field is mirrored into both of the Erlang gateway's vendoredagent.protocopies and forwarded untouched on the command-fanout path, the same way the existing Guardianpayloadfield is. Because gpb silently drops any field a gateway build's vendored proto doesn't declare, the gateway now advertises the literalcommand_dispatch_tag_v1inStreamStatusNotification.wire_capabilities(a new field on the same connect-time message the gateway already sends) so the server can tell a gateway that would strip the tag apart from one that carries it end to end. -
Weekly leaked test-database sweep (#1367). The
cache-prune.ymlmaintenance run now sweeps leaked...
Yuzu v0.14.0-rc2
Added
-
ADR-0031 operator surface: server wiring, MCP twins, and the authenticated
upload protocol replace the legacy file-retrieval stub (PR1.5c + PR1.6c).
PluginConfigStoreandUploadGrantStore(PR1.5b/PR1.6a) are now constructed
at server startup and wired to their REST routes
(/api/v1/plugin-config/*,/api/v1/upload-grants*) and to eight new MCP
tools (get/list/set/delete_plugin_config,set/delete_plugin_secret,
get/set_plugin_kill_switch), alongside the three upload-grant MCP twins
(mint/list/revoke_upload_grant) already shipped in #3135 — all eleven
carry the same securable, operation, and audit verb as their REST twin. The
agent-authenticated upload session endpoints (POST /api/v1/uploads,PUT .../chunk,GET .../{upload_id},POST .../commit,DELETE .../{upload_id}) are documented in the OpenAPI spec but deliberately have
no MCP twin — they authenticate on a grant/session bearer credential, never
an operator session. The legacy, unauthenticatedPOST /api/v1/file-retrievalstub (accepted a body-suppliedagent_id, stored
nothing) is removed; exactly one handler now serves file retrieval.
ScheduleRunner's arming re-check (PR1.5a) is wired to a real
authorization lookup: a schedule whose creator has since lost the required
permission for its targetplugin.actionno longer fires, on both the
auto and approval-gated paths.PluginConfigStore's secret column is now
enrolled in the live KEK rotation/rewrap/status surface alongside the auth
store's — an operator's/secrets/kek/rotateor/rewrapcall re-wraps
plugin secrets too, closing the gapdocs/adr/3005-plugin-config-store.md
recorded as open. (PR1.5c/PR1.6c) -
Plugin config/secret plane + per-action kill switch. Born-on-Postgres
PluginConfigStore
(schemaplugin_config_store) gives later default-off plugins a place to keep operator
configuration, envelope-encrypted secrets (SecretCodec, enrolled in the live KEK-rotation
surface), and a per-plugin or per-action kill switch. REST surface at
/api/v1/plugin-config/*: list/get/set/delete config (PluginConfig:Read/Write/Delete),
set/delete secret (PluginSecret:Write/Delete, write-only — no route ever returns a secret's
plaintext), get/set kill switch (PluginConfig:Read/Write). Kill-switch evaluation is
fail-closed: a degraded store reads as "disabled", never "enabled". Agent-side secret delivery
is a deliberately open item — seedocs/adr/3005-plugin-config-store.md. (PR1.5b) -
Typed schedule parameters + arming re-check on every fire path
(PR1.5a).POST /api/schedulesnow accepts an optionalparameters
object (string/number/boolean values only; nested objects/arrays, an
empty/underscore-prefixed key, more than 32 keys, or a canonical
serialization over 4KB are rejected with a400). The stored form is
canonical — keys sorted, so the same logical parameters always serialize
identically regardless of the order the caller wrote them in — and reaches
the scheduled dispatch exactly as a real
std::unordered_map<std::string,std::string>, replacing the hardcoded
empty"{}"ScheduleRunnerpreviously sent on every fire. Parameters are
set at creation only; there is no update route (delete and recreate to
change them). Existing schedules migrate forward with the canonical empty
object and are unaffected.
Separately,ScheduleRunner::fire()now re-verifies the arming principal's
authority (Deps::arming_check) BEFORE branching on approval, so the
re-check covers both the direct-dispatch (approval_mode=="auto") path
and the approval-gated path — not only the latter. The callback is
fail-closed: unset, it denies the fire, audits the denial (principal +
targetplugin.action), counts it, and still advances the schedule so a
permanently-denied schedule cannot spin. It sits in front of the existing
approval-ticket flow (ApprovalManager/fire_with_approval) as an
additional gate, not a replacement for it. -
One-time upload grant + authenticated chunked artifact receive (CC-06 fix, server side). The
server gains a new/api/v1/upload-grants(mint/list/revoke, operator-gated) +/api/v1/uploads
(session open, chunked PUT, status/resume, commit, cancel) REST surface backed by a new
born-PostgresUploadGrantStore, replacing the trust model of the old
POST /api/v1/file-retrievalstub — which accepted a client-assertedagent_idand destination
path with no real authentication and never durably wrote the uploaded bytes. An operator mints a
single-use grant naming the agent/source/size/hash bounds; redeeming it (atomically, exactly
once) opens an upload session whose own credential authenticates every chunk. Neither credential
is ever stored in plaintext (SHA-256 digest only, one-time reveal at mint/session-open). See
docs/adr/3004-artifact-blob-storage.mdfor the design. -
Cross-fragment capability-catalogue drift gate (PR1.9). Two new hermetic tests keep the five per-plugin-group
capability_decls/plugin_action_catalogue_*.hppfragments pluscore_dispatch_capabilities.hpphonest as a whole, since no single fragment's author can see the other five.tests/test_capability_catalogue_complete.py(python3 tests/test_capability_catalogue_complete.py) parses every plugin'sactions()override and cross-references it against all six catalogue sources, naming the exactplugin.actionwhen a plugin declares an action with no catalogue row, a fragment declares a row no plugin actually has, or the sameplugin.actionis declared by more than one source.tests/unit/server/test_capability_catalogue.cpp([server][dispatch][capability]) composes aCommandCapabilityRegistryover all six sources and asserts the catalogue-wide invariants: every row'srisk_tieris at or above its operation's floor, everysecurable/operationis onerbac_store.cppactually seeds, everyDestructiverow isIrreversible,system_reservedis true only forcore_dispatch_capabilities.hpprows,classify()resolves every declared pair, and a locally-constructed duplicate span makes the registry reportAmbiguous. -
Command capability classification core (PR1.9a). New header-only
server/core/src/command_capability.hpp/command_capability_parsers.hppdefine theplugin.actionclassification vocabulary (DispatchClass,Mutability) and aCommandCapabilityRegistrythat composes several independently-authored capability fragments and classifies a dispatch case-insensitively — an unknownplugin.actionis alwaysUnclassified, never a permissive default, and the sameplugin.actiondeclared by two fragments isAmbiguous, never first-wins. Also ships the frozenv1|<class>|<mutability>|<plan_hash_hex>dispatch-tag wire grammar (encode/decode) and an order-invariantcompute_plan_hash.capability_decls/core_dispatch_capabilities.hppdeclares the three system-initiated dispatches (tar.fleet_snapshot,__guard__.push_rules,asset_tags.sync). Not yet wired into a live dispatch path — a future PR consumes this registry from the dispatch chokepoint.authz_model.hppadditionally gains three newCapabilitySeedrows (PluginConfig,PluginSecret,UploadGrant), reusing only existingOperationenumerators.RbacStore::seed_defaults()seeds the same three securables intorbac.db— Administrator gets full CRUD via the existing loop, PlatformEngineer gets Read/Write/Delete on all three, and Operator gets Read onPluginConfig/UploadGrantonly, neverPluginSecret— closing the gap (peer finding PLAN-001) where a securable absent from the live seed cannot receive a default grant, denying even an Administrator on a fresh RBAC-enabled install. -
Dispatch classification tag on the wire, with a gateway wire-capability handshake (CC-03).
CommandRequestnow carriesdispatch_tag(field 9), the opaquev1|<class>|<mutability>|<plan_hash_hex>token the dispatch chokepoint uses to classify a command as read-only, mutating, or destructive. The field is mirrored into both of the Erlang gateway's vendoredagent.protocopies and forwarded untouched on the command-fanout path, the same way the existing Guardianpayloadfield is. Because gpb silently drops any field a gateway build's vendored proto doesn't declare, the gateway now advertises the literalcommand_dispatch_tag_v1inStreamStatusNotification.wire_capabilities(a new field on the same connect-time message the gateway already sends) so the server can tell a gateway that would strip the tag apart from one that carries it end to end. -
Weekly leaked test-database sweep (#1367). The
cache-prune.ymlmaintenance run now sweeps leakedyuzu_test_*databases off every persistent self-hosted CI Postgres instance (scripts/ci/sweep-test-databases.sh): epoch-named databases age by the same >6h server-clock rule as the in-suite sweeper, and names the epoch sweeper can never parse (pre-epoch format, implausible clock stamps) are reclaimed once the datdir'sPG_VERSIONmtime exceeds 7 days with zero active backends — closing the "idle box never sweeps" and "permanently unsweepable names" gaps. The same round fixed the Windows prune job's silent no-op (findresolved to System32's, not coreutils') and pinnedclean: falseon the new checkout steps so the weekly job can't wipe the runners' warm build trees. -
macOS crash reports now carry the application version.
.ipscrash and hang reports route the app'sCFBundleShortVersionString(from the report headerapp_version, falling back to the bodybundleInfo) through the sharedcanon_versionnormaliser into the DEX signal's version field, so macOS crashes join the per-(app, version)reliability identity the same way Windows crashes do. Unversioned binaries (unsigned CLI tools) report an empty version, exactly as store/packaged apps already do on...
Yuzu v0.13.0
Warning
Known issue: the yuzu-gateway image crash-loops on recent Intel CPUs (#2150).
On AMX-capable Intel hosts (Sapphire Rapids and newer, including AWS c7i, m7i and r7i instances), the 0.13.0 gateway container exits at startup and keeps restarting. Its log shows sys_sigaltstack(): Internal error: Failed to set alternate signal stack, and docker ps shows Restarting (134). To check a host, run grep -m1 -o amx_tile /proc/cpuinfo: if it prints amx_tile, the host is affected.
The cause is that the image runs on Alpine 3.24 (musl 1.2.6), which rejects the signal stack size Erlang/OTP 28 requests on those CPUs. The agent, the server and the yuzu-gateway-chisel image are not affected.
Until you can move to an image carrying the fix (0.14.0, planned for 2026-10-10):
- run the gateway on a host without AMX, or
- for a single-node gateway only, use
yuzu-gateway-chisel:0.13.0. Do not use the chisel image for a clustered gateway: it lacks the entrypoint that resolves the node's cluster address, so the node starts but never joins.
No 0.13.x release has the fix, so rolling back within 0.13.x does not help.
Added
- Recurring instruction schedules now actually fire (#1191).
ScheduleEngine::evaluate_due/advance_schedulehad no production caller — schedules persisted and listed but never dispatched. A newScheduleRunnerpoller (thePolicyEvaluator/PreflightRunnershape, 30s tick, joined before the stores) closes the gap: due schedules fire through the same tracked dispatch path as operator-initiated commands, the approval gate is never bypassed (requires_approvalOR definitionapproval_mode != "auto", one ticket per occurrence, one-approval == one-run via the occurrence anchor), and every outcome is audited (instruction.schedule_fired,instruction.approval_required) and counted (yuzu_schedule_{fires,fire_failures,approvals_submitted,tick_errors}_total). Hardened against adversarial review before merge:POST /api/schedulesrequires BOTHSchedule:WriteandExecution:Execute— a created schedule is a fleet-wide command-dispatch primitive with no further permission check at fire time — and re-enabling a disabled schedule requiresExecution:Executetoo (disabling never does, so a runaway schedule can always be stopped); delete and enable/disable are owner-scoped (created_by); one approval ticket is now scoped to the single schedule occurrence that requested it (approvals.schedule_id), so two schedules sharing the same (creator, definition, scope) can no longer settle on each other's ticket. - MCP agentic write surface + approval tickets (#289, R2). Five MCP write tools
are now dispatched:set_tag,delete_tag,approve_request,reject_request,
andquarantine_device(records the quarantine and dispatches live isolation).
Approval-gated tools use a ticket-then-recall flow: the first call returns
kApprovalRequired(-32006) withapproval_id+status_url; after a second
principal approves, the caller re-invokes with theapproval_idand the ticket is
consumed exactly once (args-bound, replay-rejected). Approval ids are CSPRNG-drawn;
the mint is deduplicated so a token cannot flood the shared pending-approval queue.
Review-round hardening (PR #1796):quarantine_deviceisSecurity:Executeon BOTH
transports and the supervised approval-policy rule moved with it — closing a mismatch
that let a supervised token quarantine via RESTPOST /api/v1/quarantinewith no
approval (the REST POST/DELETE routes now mirror-deny, #520); the three
device-targeted write tools (set_tag/delete_tag/quarantine_device) authorize
through the per-device management-group scope gate (require_scoped_permission), so
a group-confined operator cannot tag or isolate out-of-scope devices;
approved-but-unconsumed tickets expire after 7 days (a forgotten approval is no
longer a forever-valid capability); the approvals store recordsconsumed_by
(SOC 2 CC7.2 evidence chain: submitted_by → reviewed_by → consumed_by); and the
device-targeted ticket-mint audit rows carryagent_id=so SIEM can filter pending
isolation requests by endpoint. - A2 discovery surface (Issue 17.1, #1794).
GET /api/v1/discover/{permissions, instructions,routes,scope-kinds,plugins}plus five mirrored read-only MCP tools let
an agentic worker enumerate RBAC permissions, instruction schemas, REST routes,
scope-DSL kinds, and plugin actions from the live server (ETag + 304, A4-shaped
degraded paths). REST and MCP share one builder per catalog (cannot drift). - Agentic-worker self-orientation (R2 follow-up).
discover_plugins(REST + MCP,
now catalogversion: 2) enriches each action with itsparameter_schemainline
when the action has a published InstructionDefinition (joined on plugin+action), and
reportsactions_enriched_with_schema— so a worker learns how to call an action,
not just that it exists, from one request. Theexecute_instructionand
discover_pluginstool descriptions now spell out the async dispatch→poll pattern and
point at theyuzu://operating-modelresource. Measured effect: an LLM operator
driving the fleet with no hand-fed action list made zero blind parameter guesses. - A4 error-envelope completion (R2).
GET /api/v1/approvals/{id}(the approval
status_urltarget); theauth_routesdenial shapes are unified into one A4
envelope carryingcorrelation_idand the missingsecurable_type:operation; the
~156 legacyerror_jsonsites inrest_api_v1.cppnow emit the A4 envelope.
Breaking — many of those/api/v1errors were previously{"error":"<string>"}
and are now the nested A4 object{"error":{"code","message","correlation_id",…}};
a client that readerroras a string will break. Migrate toerror.code/
error.message. Seedocs/user-manual/upgrading.md. - Installed-software inventory gains package-manager fields (blob contract v2, ADR-0016).
Every row inGET /api/v1/inventory/softwareand thequery_installed_softwareMCP tool
now carrieskind(package|app),ecosystem(rpm|deb|apk|pacman|windows|macos|homebrew),
epoch,release,arch,signature_status(rpm only, from stored rpm header tags —
never a liverpm -Kverification),distro_id, anddistro_version, alongside the
original name/version/publisher/install_date. A field the ecosystem does not store is
"", never synthesised — seedocs/user-manual/inventory.mdfor the full per-ecosystem
matrix. Collection is a newinstalled_appsplugin action,list_inventory; the
operator-facinglist/query/list_per_useractions are unchanged. Newapk
(Alpine) package enumeration on the sync path. Breaking — data shift on Linux
rpm fleets:publishernow reflects the rpm PACKAGER tag (was VENDOR), andversion
is upstream-only with the release moved to its own column (was fused
VERSION-RELEASE). A saved query/dashboard filter against the oldpublisheror
fusedversionshape on rpm hosts will silently stop matching post-upgrade — see
docs/user-manual/inventory.mdbefore relying on either field in existing
automation. No wire-protocol or gateway change (the fields ride inside the
existing opaque sync blob). Upgrade note: deploy the server before agents; each
agent's first post-upgrade sync triggers one expected full resend (need_full),
phase-spread across the daily sync window, self-healing. - JIT elevation: OIDC amr-asserted MFA satisfies the second-factor
requirement. An OIDC operator can now activatePOST /api/v1/elevate
when their SSO session was authenticated with IdP MFA — asserted via the
OIDCamrclaim and seeded intoSession::mfa_verified_atat
/auth/callback(the same mechanism OIDC login step-up already used).
Closes the v1 limitation tracked in
docs/security-reviews/jit-elevation-2026-06-30.md. Eligibility and MFA
status are both keyed on auserstable row, which OIDC login does not
create — a federated-only identity needs one provisioned first (e.g.
POST /api/v1/users). The mandatory-MFA gate branches EXPLICITLY on
identity source: an OIDC session's only acceptable factor is a seeded
proof (mfa_verified_at != epoch, never merelyauth_source == "oidc",
security-F1) with the toggle on, and it can never fall through to a
local namesake account's TOTP enrollment (a hardening-round fix — the first
cut's single-flag guard allowed exactly that fallthrough, mislabeling the
audit trail). A single-factor (no-amr) SSO session is hard-denied with
its own distinct reason; a stale seeded proof still falls through to the
existing step-up freshness challenge rather than a silent grant. New toggle
--jit-oidc-amr-elevation/YUZU_JIT_OIDC_AMR_ELEVATION(default true;
--no-jit-oidc-amr-elevationdisables JIT elevation for OIDC sessions
entirely — they cannot fall back to a local TOTP step-up, since their
step-up challenge is re-SSO, not a TOTP code). A one-time INFO log line
announces the posture at boot when OIDC is configured. The
role.elevation.grantedaudit detail records the factor source
(duration_secs=<n> mfa=<oidc_amr|local_totp> expires_at=<rfc3339> justification=<text>
— the machine-parsedmfa=andexpires_at=tokens are placed before the
free-textjustification=field so a crafted justification can't forge either). - JIT admin elevation follow-ups: passive-lapse audit + absolute
expires_at(SOC 2
CC6.3/CC6.6). Closes the two residual risks tracked in
docs/security-reviews/jit-elevation-2026-06-30.md. (A) A JIT elevation window that
lapses PASSIVELY (no manualPOST /api/v1/elevate/revoke) is now audited as
role.elevation.expired— emitted LAZILY (no new background thread) by
AuthManager::reap_expired_elevation, called from theAuthRoutes::resolve_session
cookie chokepoint on the...