Skip to content

Releases: DevNullLtd/Yuzu

Yuzu v0.15.0

Choose a tag to compare

@github-actions github-actions released this 30 Oct 16:45
7937ede

Added

  • local_security_policy plugin: Windows leg and the sudoers action. Windows reads password_policy, lockout_policy and audit_policy from one read-only secedit /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 under agent.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 reports secedit:export_incomplete, never rows. sudoers (Linux and macOS) reads /etc/sudoers and /etc/sudoers.d/*, lexed as sudo 1.9.16 lexes them, and reports one row per rule clause; an unreadable file is permission_denied/unreadable, never an empty result, and Windows reports a fixed windows_has_no_sudoers row. The action is Security read-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 explicit unsupported row), read-only and in-process (never a sqlite3 CLI shellout), over an immutable descriptor with bounded schema/row/value/byte/time reads; it replaces the single unsupported/macos:planned placeholder row released in 0.14.0. The plugin is still off until an operator enables it. An agent identity without Full Disk Access reads denied for every source and reports PERMISSION_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 own TCC.db records, not what tccd enforces. Windows ships in the same release (see the privacy_permissions Windows leg entry).

  • privacy_permissions Windows leg. Reads each real profile's CapabilityAccessManager ConsentStore (the loaded HKU hive first, else the profile's NTUSER.DAT under SeBackup/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 the unsupported/windows:planned placeholder row listed under 0.14.0. The offline NTUSER.DAT load 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 still AdminOrApproval, single-target and audited). An offline mount alters the logged-off profile's hive sequence number and hash.

  • The filesystem plugin's get_acl action 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_access and, 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 a control row, 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. Windows ace rows now carry a fifth field with the inheritance flags, an undecoded ACE type prints - for its mask instead of a zero, and a failed GetAce or GetAclInformation now ends the command with an error instead of silently returning fewer rows.

  • New read-only mgmt_posture agent plugin — which management plane a device is under, on Linux and macOS (the Windows leg is not read yet). The single posture action reports facts only and changes nothing. On Linux it reads /etc/sssd/sssd.conf and /etc/sssd/conf.d/*.conf for the active SSSD domains and reports the plane as ad, ipa or none (an IPA realm in /etc/ipa/default.conf also reports ipa), plus whether /etc/krb5.keytab is present; it reports the configuration as written, not the live join state. On macOS it reads MDM enrolment and the MDM server host from profiles status -type enrollment. An SSSD configuration the agent is not allowed to read reports permission_denied with plane unknown, 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 one unsupported status row (windows:planned) until it ships.

  • network_config routes action. 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 other network_config actions; the result definition is device.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_catalogue and get_software_versions share the REST catalogue's model. GET /api/v1/inventory/software and query_installed_software gain a version filter, a source field per row and opaque cursor/next_cursor pagination; the REST route also gains q, kind, ecosystem and source filters and a format=csv|json export of up to 10000 rows (REST-only; a CSV export carries its continuation in an X-Next-Cursor header and ends with a # result_truncated_by_cap record when more rows may exist).

  • Software catalogue REST routes. GET /api/v1/inventory/software/catalogue returns the fleet software title catalogue with totals that cover every row under the active filters, first-page KPIs (headline numbers plus reporting/connected/stale device counts) and a format=csv titles export; GET /api/v1/inventory/software/catalogue/versions returns installs per version. Keyset cursors are bound to the filter set and to the hourly catalogue refresh (a stale or mismatched cursor is a 400), 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 retryable 503, 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/apps and list_dex_apps serve 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 below kDexCohortFloor devices (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) and GET /api/v1/discover/instructions carry input_schema per definition: the canonical JSON Schema that POST /api/instructions/{id}/execute enforces (or null), alongside input_schema_error (null, `parameter_schema_not_ca...

Read more

Yuzu v0.14.1

Choose a tag to compare

@github-actions github-actions released this 07 Oct 09:09
17ddbd1

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, or bundle+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 tag yuzu.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 with YUZU_UPDATE_ that it does not read, so a misspelling AFTER the YUZU_UPDATE_ prefix (for example YUZU_UPDATE_TRUST_BUNDEL) no longer leaves signing silently off. The Windows Environment-value check script in the server administration guide now also rejects non-ASCII characters in the signing entries and the binary path, and requires $stage to be 1 or 2. (#5249)

  • --postgres-dsn-file and --oidc-client-secret-file read 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 as OTA update <version> REFUSED: the update trust bundle could not be loaded, no longer reason="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 total yuzu.ota_signature_refused (so yuzu_fleet_ota_signature_refusing_agents) still includes it. New refusal: both --update-trust-bundle and --plugin-trust-bundle are 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-signature or --plugin-require-signature that 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 a vX.Y.Z[-{alpha,beta,rc}N] tag, and fails the run before any build or push when GET /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 signed SHA256SUMS and SBOMs, and Create GitHub Release then 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 in docs/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 YuzuAgent service (for example, a file in the program directory cannot be replaced, which an unattended /SUPPRESSMSGBOXES install 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 /NOSTART was 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, with sc.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 ran sc.exe start YuzuAgent or 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 say sc.exe, never plain sc, which PowerShell reads as Set-Content. (#5250)

  • A docs- or changelog-only push to main/dev now gets the required CI checks (#5320). ci.yml's push trigger dropped its paths filter, so every push runs; preflight's code_changed gate 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 was CHANGELOG.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 touching docs/os-capability-matrix.md still 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/certs out of the running server container first (Upgrading → "Docker Compose: copy /etc/yuzu/certs out 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/certs while 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.yml used 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 as docker-compose.yml since #5419) and docker-compose.reference-gateway.yml (their certs volume). 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 down then up, or --force-recreate deleted the key files, and the server then refused to start with kek_unresolvable. Those composes and the /test upgrade-test compose now mount a server-certs volume 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 named server-certs volume 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 ./certs read-only, so the server cannot write its KEK and stops at first boot with provider_failure (#5420). The path does not move, so CA key paths recorded in Postgres stay valid. The chiselled server image now creates /etc/yuzu/certs owned by the server user, and the demo stack adds a one-shot server-certs-init service 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 /test upgrade 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.yml and ran YUZU_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_VERSION was ignored, and the stack never started. The quickstart now downloads deploy/docker/docker-compose.reference.yml from the release tag, which pulls ghcr.io/devnullltd/yuzu-server and yuzu-postgres at ${YUZU_VERSION}, serves HTTPS on 8443 and keeps /etc/yuzu/certs on a named volume. It is three short steps that paste safely into bash or zsh: create the directory and a .env with the version and two different Postgres passwords, write the first admin account to yuzu-server.cfg (the server's interactive first-run setup exits when there is no terminal), then start the stack and check `/read...

Read more

Yuzu v0.14.0

Choose a tag to compare

@github-actions github-actions released this 03 Oct 22:54
50a81af

⚠️ Docker Compose users: copy /etc/yuzu/certs out of the server container before you next recreate it. In 0.14.0, deploy/docker/docker-compose.yml (the README quickstart), docker-compose.uat.yml, docker-compose.full-uat.yml, docker-compose.viz-uat.yml, docker-compose.demo.yml (with --keep), and Compose Wizard output in Plaintext mode, with "Persist generated certs" unticked, or with named volumes off and an external Postgres, keep the server's CA and secrets key-encryption key inside the container. Recreating the container deletes them: that covers an image upgrade, docker compose down then up, and up --force-recreate. The server then refuses to start (kek_unresolvable), and anything encrypted with that key, MFA secrets included, cannot be recovered. Don't recreate the server container, and don't run docker compose down -v, until you have followed the copy-out recipe in docs/user-manual/upgrading.md, section "Docker Compose: copy /etc/yuzu/certs out of the server container before you recreate it". The compose files are fixed on main and in 0.14.1 (#5370). Not affected: docker-compose.reference.yml, docker-compose.reference-gateway.yml, and non-Docker installs.

The full 0.14.0 changelog is longer than a GitHub release can hold. The list below is cut short; the complete ## [0.14.0] section is in CHANGELOG.md.

Before you upgrade

  • Windows agent: set update-signing options in the service's Environment registry value, not its binary path. Every run of the agent installer, including an upgrade pushed by SCCM, Intune or GPO, rewrites the YuzuAgent service's binary path and silently drops --update-trust-bundle and --update-require-signature from it. OTA updates are then no longer signature-checked. Windows: the service's Environment value in docs/user-manual/server-admin.md gives the commands and a check. (#5196)
  • Windows agent installer: pilot it first. It now refuses, with exit code 7 and the reason in its /LOG= file, an %ProgramData%\Yuzu\agent-certs folder it did not secure, or one holding anything other than plain files owned by Administrators or SYSTEM. If it refuses, the agent service is left running. If an endpoint ran a 0.14.0 release candidate from rc1 to rc5, see OTA update binary signing in the user manual to repair agent-certs. 0.13.0 endpoints are not affected. (#5196)
  • Linux native packages need a recent distribution. See the known issue below. The container images are the supported route on older hosts. (#5143)

Known issues

Windows server installer (#5196, #5210)

The Windows server installer adds permissions to %ProgramData%\Yuzu Server but does not remove the ones it inherits, so local users on that machine can read its contents. That includes yuzu-server.cfg (password hashes) and any TLS key copied into certs\. A silent install's /LOG= file also contains the installer's full command line, including any passwords passed to it: protect or delete it. Windows server is not a supported deployment (ADR-0035), and a service registered by this installer cannot start without a manually configured Postgres connection (#5272). If you run it anyway, restrict that folder to Administrators and SYSTEM, and do not let untrusted users sign in to the machine. A redesigned installer is planned for 0.14.1.

Linux native packages need a recent distribution (#5143, #5171)

The .deb, .rpm and tarball binaries need glibc 2.38. The server also needs GCC 15's libstdc++, and the agent GCC 14's. In practice the server runs on Ubuntu 26.04 and Fedora 42, and the agent also runs on Ubuntu 24.04. None of them runs on Ubuntu 22.04, Debian 12 or RHEL/Rocky 9: the server and agent .rpm refuse to install there, while the .deb packages install but do not start. The gateway also needs OpenSSL 3, and its .rpm does not start on Fedora 42. The container images are not affected (except that the Linux agent delivered by yuzu-agent-bundle-chisel has the same requirements); use them for the server and gateway elsewhere. Building to an older baseline is planned for 0.14.1.


Added

  • ADR-0031 operator surface: server wiring, MCP twins, and the authenticated
    upload protocol replace the legacy file-retrieval stub (PR1.5c + PR1.6c).

    PluginConfigStore and UploadGrantStore (PR1.5b/PR1.6a) are now constructed
    at server startup and wired to their REST routes
    (/api/v1/plugin-config/*, /api/v1/upload-grants*) and to eight new MCP
    tools (get/list/set/delete_plugin_config, set/delete_plugin_secret,
    get/set_plugin_kill_switch), alongside the three upload-grant MCP twins
    (mint/list/revoke_upload_grant) already shipped in #3135 — all eleven
    carry the same securable, operation, and audit verb as their REST twin. The
    agent-authenticated upload session endpoints (POST /api/v1/uploads, PUT .../chunk, GET .../{upload_id}, POST .../commit, DELETE .../{upload_id}) are documented in the OpenAPI spec but deliberately have
    no MCP twin — they authenticate on a grant/session bearer credential, never
    an operator session. The legacy, unauthenticated POST /api/v1/file-retrieval stub (accepted a body-supplied agent_id, stored
    nothing) is removed; exactly one handler now serves file retrieval.
    ScheduleRunner's arming re-check (PR1.5a) is wired to a real
    authorization lookup: a schedule whose creator has since lost the required
    permission for its target plugin.action no longer fires, on both the
    auto and approval-gated paths. PluginConfigStore's secret column is now
    enrolled in the live KEK rotation/rewrap/status surface alongside the auth
    store's — an operator's /secrets/kek/rotate or /rewrap call re-wraps
    plugin secrets too, closing the gap docs/adr/3005-plugin-config-store.md
    recorded as open. (PR1.5c/PR1.6c)

  • Plugin config/secret plane + per-action kill switch. Born-on-Postgres PluginConfigStore
    (schema plugin_config_store) gives later default-off plugins a place to keep operator
    configuration, envelope-encrypted secrets (SecretCodec, enrolled in the live KEK-rotation
    surface), and a per-plugin or per-action kill switch. REST surface at
    /api/v1/plugin-config/*: list/get/set/delete config (PluginConfig:Read/Write/Delete),
    set/delete secret (PluginSecret:Write/Delete, write-only — no route ever returns a secret's
    plaintext), get/set kill switch (PluginConfig:Read/Write). Kill-switch evaluation is
    fail-closed: a degraded store reads as "disabled", never "enabled". Agent-side secret delivery
    is a deliberately open item — see docs/adr/3005-plugin-config-store.md. (PR1.5b)

  • Typed schedule parameters + arming re-check on every fire path
    (PR1.5a).
    POST /api/schedules now accepts an optional parameters
    object (string/number/boolean values only; nested objects/arrays, an
    empty/underscore-prefixed key, more than 32 keys, or a canonical
    serialization over 4KB are rejected with a 400). The stored form is
    canonical — keys sorted, so the same logical parameters always serialize
    identically regardless of the order the caller wrote them in — and reaches
    the scheduled dispatch exactly as a real
    std::unordered_map<std::string,std::string>, replacing the hardcoded
    empty "{}" ScheduleRunner previously sent on every fire. Parameters are
    set at creation only; there is no update route (delete and recreate to
    change them). Existing schedules migrate forward with the canonical empty
    object and are unaffected.
    Separately, ScheduleRunner::fire() now re-verifies the arming principal's
    authority (Deps::arming_check) BEFORE branching on approval, so the
    re-check covers both the direct-dispatch (approval_mode=="auto") path
    and the approval-gated path — not only the latter. The callback is
    fail-closed: unset, it denies the fire, audits the denial (principal +
    target plugin.action), counts it, and still advances the schedule so a
    permanently-denied schedule cannot spin. It sits in front of the existing
    approval-ticket flow (ApprovalManager / fire_with_approval) as an
    additional gate, not a replacement for it.

  • One-time upload grant + authenticated chunked artifact receive (CC-06 fix, server side). The
    server gains a new /api/v1/upload-grants (mint/list/revoke, operator-gated) + /api/v1/uploads
    (session open, chunked PUT, status/resume, commit, cancel) REST surface backed by a new
    born-Postgres UploadGrantStore, replacing the trust model of the old
    POST /api/v1/file-retrieval stub — which accepted a client-asserted agent_id and destination
    path with no real authentication and never durably wrote the uploaded bytes. An operator mints a
    single-use grant naming the agent/source/size/hash bounds; redeeming it (atomically, exactly
    once) opens an upload session whose own credential authenticates every chunk. Neither credential
    is ever stored in plaintext (SHA-256 digest only, one-time reveal at mint/session-open). See
    docs/adr/3004-artifact-blob-storage.md for the design.

  • Cross-fragment capability-catalogue drift gate (PR1.9). Two new hermetic tests keep the five per-plugin-group capability_decls/plugin_action_catalogue_*.hpp fragments plus core_dispatch_capabilities.hpp honest as a whole, since no single fragment's author can see the other five. tests/test_capability_catalogue_complete.py (python3 tests/test_capability_catalogue_complete.py) parses every plugin's actions() override and cross-references it against all six catalogue sources, naming the exact plugin.action when a plugin declares an action with no catalogue row, a fragment declares a row no plugin actually has, or the same plugin.action is declared by more than one source. `tests/unit/server/test_...

Read more

Yuzu v0.14.0-rc7

Yuzu v0.14.0-rc7 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 03 Oct 20:18
2fbaa88

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 --append folds hotfix changelog fragments that land after a version's first release candidate into its existing section. The CHANGELOG's 0.14.0 section 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).

    PluginConfigStore and UploadGrantStore (PR1.5b/PR1.6a) are now constructed
    at server startup and wired to their REST routes
    (/api/v1/plugin-config/*, /api/v1/upload-grants*) and to eight new MCP
    tools (get/list/set/delete_plugin_config, set/delete_plugin_secret,
    get/set_plugin_kill_switch), alongside the three upload-grant MCP twins
    (mint/list/revoke_upload_grant) already shipped in #3135 — all eleven
    carry the same securable, operation, and audit verb as their REST twin. The
    agent-authenticated upload session endpoints (POST /api/v1/uploads, PUT .../chunk, GET .../{upload_id}, POST .../commit, DELETE .../{upload_id}) are documented in the OpenAPI spec but deliberately have
    no MCP twin — they authenticate on a grant/session bearer credential, never
    an operator session. The legacy, unauthenticated POST /api/v1/file-retrieval stub (accepted a body-supplied agent_id, stored
    nothing) is removed; exactly one handler now serves file retrieval.
    ScheduleRunner's arming re-check (PR1.5a) is wired to a real
    authorization lookup: a schedule whose creator has since lost the required
    permission for its target plugin.action no longer fires, on both the
    auto and approval-gated paths. PluginConfigStore's secret column is now
    enrolled in the live KEK rotation/rewrap/status surface alongside the auth
    store's — an operator's /secrets/kek/rotate or /rewrap call re-wraps
    plugin secrets too, closing the gap docs/adr/3005-plugin-config-store.md
    recorded as open. (PR1.5c/PR1.6c)

  • Plugin config/secret plane + per-action kill switch. Born-on-Postgres PluginConfigStore
    (schema plugin_config_store) gives later default-off plugins a place to keep operator
    configuration, envelope-encrypted secrets (SecretCodec, enrolled in the live KEK-rotation
    surface), and a per-plugin or per-action kill switch. REST surface at
    /api/v1/plugin-config/*: list/get/set/delete config (PluginConfig:Read/Write/Delete),
    set/delete secret (PluginSecret:Write/Delete, write-only — no route ever returns a secret's
    plaintext), get/set kill switch (PluginConfig:Read/Write). Kill-switch evaluation is
    fail-closed: a degraded store reads as "disabled", never "enabled". Agent-side secret delivery
    is a deliberately open item — see docs/adr/3005-plugin-config-store.md. (PR1.5b)

  • Typed schedule parameters + arming re-check on every fire path
    (PR1.5a).
    POST /api/schedules now accepts an optional parameters
    object (string/number/boolean values only; nested objects/arrays, an
    empty/underscore-prefixed key, more than 32 keys, or a canonical
    serialization over 4KB are rejected with a 400). The stored form is
    canonical — keys sorted, so the same logical parameters always serialize
    identically regardless of the order the caller wrote them in — and reaches
    the scheduled dispatch exactly as a real
    std::unordered_map<std::string,std::string>, replacing the hardcoded
    empty "{}" ScheduleRunner previously sent on every fire. Parameters are
    set at creation only; there is no update route (delete and recreate to
    change them). Existing schedules migrate forward with the canonical empty
    object and are unaffected.
    Separately, ScheduleRunner::fire() now re-verifies the arming principal's
    authority (Deps::arming_check) BEFORE branching on approval, so the
    re-check covers both the direct-dispatch (approval_mode=="auto") path
    and the approval-gated path — not only the latter. The callback is
    fail-closed: unset, it denies the fire, audits the denial (principal +
    target plugin.action), counts it, and still advances the schedule so a
    permanently-denied schedule cannot spin. It sits in front of the existing
    approval-ticket flow (ApprovalManager / fire_with_approval) as an
    additional gate, not a replacement for it.

  • One-time upload grant + authenticated chunked artifact receive (CC-06 fix, server side). The
    server gains a new /api/v1/upload-grants (mint/list/revoke, operator-gated) + /api/v1/uploads
    (session open, chunked PUT, status/resume, commit, cancel) REST surface backed by a new
    born-Postgres UploadGrantStore, replacing the trust model of the old
    POST /api/v1/file-retrieval stub — which accepted a client-asserted agent_id and destination
    path with no real authentication and never durably wrote the uploaded bytes. An operator mints a
    single-use grant naming the agent/source/size/hash bounds; redeeming it (atomically, exactly
    once) opens an upload session whose own credential authenticates every chunk. Neither credential
    is ever stored in plaintext (SHA-256 digest only, one-time reveal at mint/session-open). See
    docs/adr/3004-artifact-blob-storage.md for the design.

  • Cross-fragment capability-catalogue drift gate (PR1.9). Two new hermetic tests keep the five per-plugin-group capability_decls/plugin_action_catalogue_*.hpp fragments plus core_dispatch_capabilities.hpp honest as a whole, since no single fragment's author can see the other five. tests/test_capability_catalogue_complete.py (python3 tests/test_capability_catalogue_complete.py) parses every plugin's actions() override and cross-references it against all six catalogue sources, naming the exact plugin.action when a plugin declares an action with no catalogue row, a fragment declares a row no plugin actually has, or the same plugin.action is declared by more than one source. tests/unit/server/test_capability_catalogue.cpp ([server][dispatch][capability]) composes a CommandCapabilityRegistry over all six sources and asserts the catalogue-wide invariants: every row's risk_tier is at or above its operation's floor, every securable/operation is one rbac_store.cpp actually seeds, every Destructive row is Irreversible, system_reserved is true only for core_dispatch_capabilities.hpp rows, classify() resolves every declared pair, and a locally-constructed duplicate span makes the registry report Ambiguous.

  • Command capability classification core (PR1.9a). New header-only server/core/src/command_capability.hpp / command_capability_parsers.hpp define the plugin.action classification vocabulary (DispatchClass, Mutability) and a CommandCapabilityRegistry that composes several independently-authored capability fragments and classifies a dispatch case-insensitively — an unknown plugin.action is always Unclassified, never a permissive default, and the same plugin.action declared by two fragments is Ambiguous, never first-wins. Also ships the frozen v1|<class>|<mutability>|<plan_hash_hex> dispatch-tag wire grammar (encode/decode) and an order-invariant compute_plan_hash. capability_decls/core_dispatch_capabilities.hpp declares the three system-initiated dispatc...

Read more

Yuzu v0.14.0-rc6

Yuzu v0.14.0-rc6 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 02 Oct 16:24
ee78dca

Note (added after publication): yuzu-agent-chisel-image.cdx.json/.spdx.json and yuzu-server-chisel-image.cdx.json/.spdx.json were 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 signed SHA256SUMS. Each names the image it describes, and each matches the published 0.14.0-rc6 image: ghcr.io/devnullltd/yuzu-agent-chisel@sha256:4d0075315e6103381da58d431fee935b36d8a6e53bb30c4da6a228fd7bfc9d58 and ghcr.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-provisioned update-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_total rises, and the agent logs signature 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 /Q

    This 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 its update-trust-bundle.pem yourself, 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. If agent-certs already 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 Environment registry 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 the YuzuAgent service's binary path in full. If you added --update-trust-bundle and --update-require-signature there, it drops them without reporting anything, and OTA updates are no longer signature-checked. The service's Environment value (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's Environment value in docs/user-manual/server-admin.md gives 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).

    PluginConfigStore and UploadGrantStore (PR1.5b/PR1.6a) are now constructed
    at server startup and wired to their REST routes
    (/api/v1/plugin-config/*, /api/v1/upload-grants*) and to eight new MCP
    tools (get/list/set/delete_plugin_config, set/delete_plugin_secret,
    get/set_plugin_kill_switch), alongside the three upload-grant MCP twins
    (mint/list/revoke_upload_grant) already shipped in #3135 — all eleven
    carry the same securable, operation, and audit verb as their REST twin. The
    agent-authenticated upload session endpoints (POST /api/v1/uploads, PUT .../chunk, GET .../{upload_id}, POST .../commit, DELETE .../{upload_id}) are documented in the OpenAPI spec but deliberately have
    no MCP twin — they authenticate on a grant/session bearer credential, never
    an operator session. The legacy, unauthenticated POST /api/v1/file-retrieval stub (accepted a body-supplied agent_id, stored
    nothing) is removed; exactly one handler now serves file retrieval.
    ScheduleRunner's arming re-check (PR1.5a) is wired to a real
    authorization lookup: a schedule whose creator has since lost the required
    permission for its target plugin.action no longer fires, on both the
    auto and approval-gated paths. PluginConfigStore's secret column is now
    enrolled in the live KEK rotation/rewrap/status surface alongside the auth
    store's — an operator's /secrets/kek/rotate or /rewrap call re-wraps
    plugin secrets too, closing the gap docs/adr/3005-plugin-config-store.md
    recorded as open. (PR1.5c/PR1.6c)

  • Plugin config/secret plane + per-action kill switch. Born-on-Postgres PluginConfigStore
    (schema plugin_config_store) gives later default-off plugins a place to keep operator
    configuration, envelope-encrypted secrets (SecretCodec, enrolled in the live KEK-rotation
    surface), and a per-plugin or per-action kill switch. REST surface at
    /api/v1/plugin-config/*: list/get/set/delete config (PluginConfig:Read/Write/Delete),
    set/delete secret (PluginSecret:Write/Delete, write-only — no route ever returns a secret's
    plaintext), get/set kill switch (PluginConfig:Read/Write). Kill-switch evaluation is
    fail-closed: a degraded store reads as "disabled", never "enabled". Agent-side secret delivery
    is a deliberately open item — see docs/adr/3005-plugin-config-store.md. (PR1.5b)

  • Typed schedule parameters + arming re-check on every fire path
    (PR1.5a).
    POST /api/schedules now accepts an optional parameters
    object (string/number/boolean values only; nested objects/arrays, an
    empty/underscore-prefixed key, more than 32 keys, or a canonical
    serialization over 4KB are rejected with a 400). The stored form is
    canonical — keys sorted, so the same logical parameters always serialize
    identically regardless of the order the caller w...

Read more

Yuzu v0.14.0-rc5

Yuzu v0.14.0-rc5 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 01 Oct 15:41
7fe9543

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-gateway image crashed at startup with sys_sigaltstack(): Internal error: Failed to set alternate signal stack and kept restarting (docker ps shows Restarting (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" in docs/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/metrics returned 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 whole yuzu-gateway alert group were blind, including the management-plane authentication alerts. YuzuGatewayDown also 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 raises YuzuGatewayMgmtAuthRejected. (#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/plugins to your agent service's command:. (#5134)

Also not yet in the CHANGELOG (from v0.14.0-rc3)

  • Pre-release .deb downloads verify against SHA256SUMS. RC .deb files are named with the tag's version; the package version is still 0.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 .pkg starts. (#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).

    PluginConfigStore and UploadGrantStore (PR1.5b/PR1.6a) are now constructed
    at server startup and wired to their REST routes
    (/api/v1/plugin-config/*, /api/v1/upload-grants*) and to eight new MCP
    tools (get/list/set/delete_plugin_config, set/delete_plugin_secret,
    get/set_plugin_kill_switch), alongside the three upload-grant MCP twins
    (mint/list/revoke_upload_grant) already shipped in #3135 — all eleven
    carry the same securable, operation, and audit verb as their REST twin. The
    agent-authenticated upload session endpoints (POST /api/v1/uploads, PUT .../chunk, GET .../{upload_id}, POST .../commit, DELETE .../{upload_id}) are documented in the OpenAPI spec but deliberately have
    no MCP twin — they authenticate on a grant/session bearer credential, never
    an operator session. The legacy, unauthenticated POST /api/v1/file-retrieval stub (accepted a body-supplied agent_id, stored
    nothing) is removed; exactly one handler now serves file retrieval.
    ScheduleRunner's arming re-check (PR1.5a) is wired to a real
    authorization lookup: a schedule whose creator has since lost the required
    permission for its target plugin.action no longer fires, on both the
    auto and approval-gated paths. PluginConfigStore's secret column is now
    enrolled in the live KEK rotation/rewrap/status surface alongside the auth
    store's — an operator's /secrets/kek/rotate or /rewrap call re-wraps
    plugin secrets too, closing the gap docs/adr/3005-plugin-config-store.md
    recorded as open. (PR1.5c/PR1.6c)

  • Plugin config/secret plane + per-action kill switch. Born-on-Postgres PluginConfigStore
    (schema plugin_config_store) gives later default-off plugins a place to keep operator
    configuration, envelope-encrypted secrets (SecretCodec, enrolled in the live KEK-rotation
    surface), and a per-plugin or per-action kill switch. REST surface at
    /api/v1/plugin-config/*: list/get/set/delete config (PluginConfig:Read/Write/Delete),
    set/delete secret (PluginSecret:Write/Delete, write-only — no route ever returns a secret's
    plaintext), get/set kill switch (PluginConfig:Read/Write). Kill-switch evaluation is
    fail-closed: a degraded store reads as "disabled", never "enabled". Agent-side secret delivery
    is a deliberately open item — see docs/adr/3005-plugin-config-store.md. (PR1.5b)

  • Typed schedule parameters + arming re-check on every fire path
    (PR1.5a).
    POST /api/schedules now accepts an optional parameters
    object (string/number/boolean values only; nested objects/arrays, an
    empty/underscore-prefixed key, more than 32 keys, or a canonical
    serialization over 4KB are rejected with a 400). The stored form is
    canonical — keys sorted, so the same logical parameters always serialize
    identically regardless of the order the caller wrote them in — and reaches
    the scheduled dispatch exactly as a real
    std::unordered_map<std::string,std::string>, replacing the hardcoded
    empty "{}" ScheduleRunner previously sent on every fire. Parameters are
    set at creation only; there is no update route (delete and recreate to
    change them). Existing schedules migrate forward with the canonical empty
    object and are unaffected.
    Separately, ScheduleRunner::fire() now re-verifies the arming principal's
    authority (Deps::arming_check) BEFORE branching on approval, so the
    re-check covers both the direct-dispatch (approval_mode=="auto") path
    and the approval-gated path — not only the latter. The callback is
    fail-closed: unset, it denies the fire, audits the denial (principal +
    target plugin.action), counts it, and still advances the schedule so a
    permanently-denied schedule cannot spin. It sits in front of the existing
    approval-ticket flow (ApprovalManager / fire_with_approval) as an
    additional gate, not a replacement for it.

  • One-time upload grant + authenticated chunked artifact receive (CC-06 fix, server side). The
    server gains a new /api/v1/upload-grants (mint/list/revoke, operator-gated) + /api/v1/uploads
    (session open, chunked PUT, status/resume, commit, cancel) REST surface backed by a new
    born-Postgres UploadGrantStore, replacing the trust model of the old
    POST /api/v1/file-retrieval stub — which accepted a client-asserted agent_id and destination
    path with no real authentication and never durably wrote the uploaded bytes. An operator mints a
    single-use grant naming the agent/source/size/hash bounds; redeeming it (atomically, exactly
    once) opens an upload session whose own credential authenticates every chunk. Neither credential
    is ever stored in plaintext (SHA-256 digest only, one-time reveal at mint/session-open). See
    docs/adr/3004-artifact-blob-storage.md for the design.

  • Cross-fragment capability-catalogue drift gate (PR1.9). Two new hermetic tests keep the five per-plugin-group capability_decls/plugin_action_catalogue_*.hpp fragments plus core_dispatch_capabilities.hpp honest as a whole, since no single fragment's author can see the other five. tests/test_capability_catalogue_complete.py (python3 tests/test_capability_catalogue_complete.py) parses every plugin's actions() override and cross-references it against all six catalogue sources, naming the exact plugin.action when a plugin declares an action with no catalogue row, a fragment declares a row no plugin actually has, or the same plugin.action is declared by more than one source. tests/unit/server/test_capability_catalogue.cpp ([server][dispatch][capability]) composes a CommandCapabilityRegistry over all six sources and asserts the catalogue-wide invariants: every row's risk_tier is at or above its operation's floor, every securable/operation is one rbac_store.cpp actually seeds, every Destructive row is Irreversible, system_reserved is true only for core_dispatch_capabilities.hpp rows, classify() resolves every declared pair, and a locally-constructed duplicate span makes the registry report Ambiguous.

  • **Command...

Read more

Yuzu v0.14.0-rc4

Yuzu v0.14.0-rc4 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 01 Oct 10:33
84cea7f

Note (added after publication): yuzu-agent-chisel-image.cdx.json and .spdx.json were 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 signed SHA256SUMS. Each one names the image it describes, ghcr.io/devnullltd/yuzu-agent-chisel@sha256:c70d34adc05f4c9c71cac6efd47aef05eaba7f5ff8524bca2174b798b3ccff48, which is the published 0.14.0-rc4 image.

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/metrics returned 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 whole yuzu-gateway alert group were blind, including the management-plane authentication alerts. YuzuGatewayDown also 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 raises YuzuGatewayMgmtAuthRejected. (#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/plugins to your agent service's command:. (#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 .deb downloads verify against SHA256SUMS. RC .deb files are named with the tag's version; the package version is still 0.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 .pkg starts. (#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).

    PluginConfigStore and UploadGrantStore (PR1.5b/PR1.6a) are now constructed
    at server startup and wired to their REST routes
    (/api/v1/plugin-config/*, /api/v1/upload-grants*) and to eight new MCP
    tools (get/list/set/delete_plugin_config, set/delete_plugin_secret,
    get/set_plugin_kill_switch), alongside the three upload-grant MCP twins
    (mint/list/revoke_upload_grant) already shipped in #3135 — all eleven
    carry the same securable, operation, and audit verb as their REST twin. The
    agent-authenticated upload session endpoints (POST /api/v1/uploads, PUT .../chunk, GET .../{upload_id}, POST .../commit, DELETE .../{upload_id}) are documented in the OpenAPI spec but deliberately have
    no MCP twin — they authenticate on a grant/session bearer credential, never
    an operator session. The legacy, unauthenticated POST /api/v1/file-retrieval stub (accepted a body-supplied agent_id, stored
    nothing) is removed; exactly one handler now serves file retrieval.
    ScheduleRunner's arming re-check (PR1.5a) is wired to a real
    authorization lookup: a schedule whose creator has since lost the required
    permission for its target plugin.action no longer fires, on both the
    auto and approval-gated paths. PluginConfigStore's secret column is now
    enrolled in the live KEK rotation/rewrap/status surface alongside the auth
    store's — an operator's /secrets/kek/rotate or /rewrap call re-wraps
    plugin secrets too, closing the gap docs/adr/3005-plugin-config-store.md
    recorded as open. (PR1.5c/PR1.6c)

  • Plugin config/secret plane + per-action kill switch. Born-on-Postgres PluginConfigStore
    (schema plugin_config_store) gives later default-off plugins a place to keep operator
    configuration, envelope-encrypted secrets (SecretCodec, enrolled in the live KEK-rotation
    surface), and a per-plugin or per-action kill switch. REST surface at
    /api/v1/plugin-config/*: list/get/set/delete config (PluginConfig:Read/Write/Delete),
    set/delete secret (PluginSecret:Write/Delete, write-only — no route ever returns a secret's
    plaintext), get/set kill switch (PluginConfig:Read/Write). Kill-switch evaluation is
    fail-closed: a degraded store reads as "disabled", never "enabled". Agent-side secret delivery
    is a deliberately open item — see docs/adr/3005-plugin-config-store.md. (PR1.5b)

  • Typed schedule parameters + arming re-check on every fire path
    (PR1.5a).
    POST /api/schedules now accepts an optional parameters
    object (string/number/boolean values only; nested objects/arrays, an
    empty/underscore-prefixed key, more than 32 keys, or a canonical
    serialization over 4KB are rejected with a 400). The stored form is
    canonical — keys sorted, so the same logical parameters always serialize
    identically regardless of the order the caller wrote them in — and reaches
    the scheduled dispatch exactly as a real
    std::unordered_map<std::string,std::string>, replacing the hardcoded
    empty "{}" ScheduleRunner previously sent on every fire. Parameters are
    set at creation only; there is no update route (delete and recreate to
    change them). Existing schedules migrate forward with the canonical empty
    object and are unaffected.
    Separately, ScheduleRunner::fire() now re-verifies the arming principal's
    authority (Deps::arming_check) BEFORE branching on approval, so the
    re-check covers both the direct-dispatch (approval_mode=="auto") path
    and the approval-gated path — not only the latter. The callback is
    fail-closed: unset, it denies the fire, audits the denial (principal +
    target plugin.action), counts it, and still advances the schedule so a
    permanently-denied schedule cannot spin. It sits in front of the existing
    approval-ticket flow (ApprovalManager / fire_with_approval) as an
    additional gate, not a replacement for it.

  • One-time upload grant + authenticated chunked artifact receive (CC-06 fix, server side). The
    server gains a new /api/v1/upload-grants (mint/list/revoke, operator-gated) + /api/v1/uploads
    (session open, chunked PUT, status/resume, commit, cancel) REST surface backed by a new
    born-Postgres UploadGrantStore, replacing the trust model of the old
    POST /api/v1/file-retrieval stub — which accepted a client-asserted agent_id and destination
    path with no real authentication and never durably wrote the uploaded bytes. An operator mints a
    single-use grant naming the agent/source/size/hash bounds; redeeming it (atomically, exactly
    once) opens an upload session whose own credential authenticates every chunk. Neither credential
    is ever stored in plaintext (SHA-256 digest only, one-time reveal at mint/session-open). See
    docs/adr/3004-artifact-blob-storage.md for the design.

  • Cross-fragment capability-catalogue drift gate (PR1.9). Two new hermetic tests keep the five per-plugin-group capability_decls/plugin_action_catalogue_*.hpp fragments plus core_dispatch_capabilities.hpp honest as a whole, since no single fragment's author can see the other five. tests/test_capability_catalogue_complete.py (python3 tests/test_capability_catalogue_complete.py) parses every plugin's actions() override and cross-references it against all six catalogue sources, naming the exact plugin.action when a plugin declares an action with no catalogue row, a fragment declares a row no plugin actually has, or the same plugin.action is declared by more than one source. tests/unit/server/test_capability_catalogue.cpp ([server][dispatch][capability]) composes a CommandCapabilityRegistry over all six sources and asserts the catalogue-wide invariants: every row's risk_tier is at or above its operation's floor, every securable/operation is one rbac_store.cpp actually seeds, every Destructive row is Irreversible, system_reserved is true only for core_dispatch_capabilities.hpp rows, classify() resolves every declared pair, and a locally-constructed duplicate span makes the registry report Ambiguous.

  • Command capability classification core (PR1.9a). New header-only server/core/src/command_capability.hpp / command_capability_parsers.hpp define the plugin.action classification vocabulary (DispatchClass, Mutability) and a CommandCapabilityRegistry that composes several independently-authored capability fragments and classifies a dispatch case-insensitively — an unknown plugin.action is always Unclassified, never a permissive default, and the same plugin.action declared by two fragments is Ambiguous, never first-wins. Also ships the frozen `v1||...

Read more

Yuzu v0.14.0-rc3

Yuzu v0.14.0-rc3 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 30 Sep 09:01
ba965b5

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 .deb downloads now verify against SHA256SUMS. RC .deb files are named with the tag's version (yuzu-agent_0.14.0-rc3_amd64.deb); the package's own version is still 0.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 .pkg starts 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).

    PluginConfigStore and UploadGrantStore (PR1.5b/PR1.6a) are now constructed
    at server startup and wired to their REST routes
    (/api/v1/plugin-config/*, /api/v1/upload-grants*) and to eight new MCP
    tools (get/list/set/delete_plugin_config, set/delete_plugin_secret,
    get/set_plugin_kill_switch), alongside the three upload-grant MCP twins
    (mint/list/revoke_upload_grant) already shipped in #3135 — all eleven
    carry the same securable, operation, and audit verb as their REST twin. The
    agent-authenticated upload session endpoints (POST /api/v1/uploads, PUT .../chunk, GET .../{upload_id}, POST .../commit, DELETE .../{upload_id}) are documented in the OpenAPI spec but deliberately have
    no MCP twin — they authenticate on a grant/session bearer credential, never
    an operator session. The legacy, unauthenticated POST /api/v1/file-retrieval stub (accepted a body-supplied agent_id, stored
    nothing) is removed; exactly one handler now serves file retrieval.
    ScheduleRunner's arming re-check (PR1.5a) is wired to a real
    authorization lookup: a schedule whose creator has since lost the required
    permission for its target plugin.action no longer fires, on both the
    auto and approval-gated paths. PluginConfigStore's secret column is now
    enrolled in the live KEK rotation/rewrap/status surface alongside the auth
    store's — an operator's /secrets/kek/rotate or /rewrap call re-wraps
    plugin secrets too, closing the gap docs/adr/3005-plugin-config-store.md
    recorded as open. (PR1.5c/PR1.6c)

  • Plugin config/secret plane + per-action kill switch. Born-on-Postgres PluginConfigStore
    (schema plugin_config_store) gives later default-off plugins a place to keep operator
    configuration, envelope-encrypted secrets (SecretCodec, enrolled in the live KEK-rotation
    surface), and a per-plugin or per-action kill switch. REST surface at
    /api/v1/plugin-config/*: list/get/set/delete config (PluginConfig:Read/Write/Delete),
    set/delete secret (PluginSecret:Write/Delete, write-only — no route ever returns a secret's
    plaintext), get/set kill switch (PluginConfig:Read/Write). Kill-switch evaluation is
    fail-closed: a degraded store reads as "disabled", never "enabled". Agent-side secret delivery
    is a deliberately open item — see docs/adr/3005-plugin-config-store.md. (PR1.5b)

  • Typed schedule parameters + arming re-check on every fire path
    (PR1.5a).
    POST /api/schedules now accepts an optional parameters
    object (string/number/boolean values only; nested objects/arrays, an
    empty/underscore-prefixed key, more than 32 keys, or a canonical
    serialization over 4KB are rejected with a 400). The stored form is
    canonical — keys sorted, so the same logical parameters always serialize
    identically regardless of the order the caller wrote them in — and reaches
    the scheduled dispatch exactly as a real
    std::unordered_map<std::string,std::string>, replacing the hardcoded
    empty "{}" ScheduleRunner previously sent on every fire. Parameters are
    set at creation only; there is no update route (delete and recreate to
    change them). Existing schedules migrate forward with the canonical empty
    object and are unaffected.
    Separately, ScheduleRunner::fire() now re-verifies the arming principal's
    authority (Deps::arming_check) BEFORE branching on approval, so the
    re-check covers both the direct-dispatch (approval_mode=="auto") path
    and the approval-gated path — not only the latter. The callback is
    fail-closed: unset, it denies the fire, audits the denial (principal +
    target plugin.action), counts it, and still advances the schedule so a
    permanently-denied schedule cannot spin. It sits in front of the existing
    approval-ticket flow (ApprovalManager / fire_with_approval) as an
    additional gate, not a replacement for it.

  • One-time upload grant + authenticated chunked artifact receive (CC-06 fix, server side). The
    server gains a new /api/v1/upload-grants (mint/list/revoke, operator-gated) + /api/v1/uploads
    (session open, chunked PUT, status/resume, commit, cancel) REST surface backed by a new
    born-Postgres UploadGrantStore, replacing the trust model of the old
    POST /api/v1/file-retrieval stub — which accepted a client-asserted agent_id and destination
    path with no real authentication and never durably wrote the uploaded bytes. An operator mints a
    single-use grant naming the agent/source/size/hash bounds; redeeming it (atomically, exactly
    once) opens an upload session whose own credential authenticates every chunk. Neither credential
    is ever stored in plaintext (SHA-256 digest only, one-time reveal at mint/session-open). See
    docs/adr/3004-artifact-blob-storage.md for the design.

  • Cross-fragment capability-catalogue drift gate (PR1.9). Two new hermetic tests keep the five per-plugin-group capability_decls/plugin_action_catalogue_*.hpp fragments plus core_dispatch_capabilities.hpp honest as a whole, since no single fragment's author can see the other five. tests/test_capability_catalogue_complete.py (python3 tests/test_capability_catalogue_complete.py) parses every plugin's actions() override and cross-references it against all six catalogue sources, naming the exact plugin.action when a plugin declares an action with no catalogue row, a fragment declares a row no plugin actually has, or the same plugin.action is declared by more than one source. tests/unit/server/test_capability_catalogue.cpp ([server][dispatch][capability]) composes a CommandCapabilityRegistry over all six sources and asserts the catalogue-wide invariants: every row's risk_tier is at or above its operation's floor, every securable/operation is one rbac_store.cpp actually seeds, every Destructive row is Irreversible, system_reserved is true only for core_dispatch_capabilities.hpp rows, classify() resolves every declared pair, and a locally-constructed duplicate span makes the registry report Ambiguous.

  • Command capability classification core (PR1.9a). New header-only server/core/src/command_capability.hpp / command_capability_parsers.hpp define the plugin.action classification vocabulary (DispatchClass, Mutability) and a CommandCapabilityRegistry that composes several independently-authored capability fragments and classifies a dispatch case-insensitively — an unknown plugin.action is always Unclassified, never a permissive default, and the same plugin.action declared by two fragments is Ambiguous, never first-wins. Also ships the frozen v1|<class>|<mutability>|<plan_hash_hex> dispatch-tag wire grammar (encode/decode) and an order-invariant compute_plan_hash. capability_decls/core_dispatch_capabilities.hpp declares the three system-initiated dispatches (tar.fleet_snapshot, __guard__.push_rules, asset_tags.sync). Not yet wired into a live dispatch path — a future PR consumes this registry from the dispatch chokepoint. authz_model.hpp additionally gains three new CapabilitySeed rows (PluginConfig, PluginSecret, UploadGrant), reusing only existing Operation enumerators. RbacStore::seed_defaults() seeds the same three securables into rbac.db — Administrator gets full CRUD via the existing loop, PlatformEngineer gets Read/Write/Delete on all three, and Operator gets Read on PluginConfig/UploadGrant only, never PluginSecret — closing the gap (peer finding PLAN-001) where a securable absent from the live seed cannot receive a default grant, denying even an Administrator on a fresh RBAC-enabled install.

  • Dispatch classification tag on the wire, with a gateway wire-capability handshake (CC-03). CommandRequest now carries dispatch_tag (field 9), the opaque v1|<class>|<mutability>|<plan_hash_hex> token the dispatch chokepoint uses to classify a command as read-only, mutating, or destructive. The field is mirrored into both of the Erlang gateway's vendored agent.proto copies and forwarded untouched on the command-fanout path, the same way the existing Guardian payload field is. Because gpb silently drops any field a gateway build's vendored proto doesn't declare, the gateway now advertises the literal command_dispatch_tag_v1 in StreamStatusNotification.wire_capabilities (a new field on the same connect-time message the gateway already sends) so the server can tell a gateway that would strip the tag apart from one that carries it end to end.

  • Weekly leaked test-database sweep (#1367). The cache-prune.yml maintenance run now sweeps leaked...

Read more

Yuzu v0.14.0-rc2

Yuzu v0.14.0-rc2 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 29 Sep 22:04
0b6d9a1

Added

  • ADR-0031 operator surface: server wiring, MCP twins, and the authenticated
    upload protocol replace the legacy file-retrieval stub (PR1.5c + PR1.6c).

    PluginConfigStore and UploadGrantStore (PR1.5b/PR1.6a) are now constructed
    at server startup and wired to their REST routes
    (/api/v1/plugin-config/*, /api/v1/upload-grants*) and to eight new MCP
    tools (get/list/set/delete_plugin_config, set/delete_plugin_secret,
    get/set_plugin_kill_switch), alongside the three upload-grant MCP twins
    (mint/list/revoke_upload_grant) already shipped in #3135 — all eleven
    carry the same securable, operation, and audit verb as their REST twin. The
    agent-authenticated upload session endpoints (POST /api/v1/uploads, PUT .../chunk, GET .../{upload_id}, POST .../commit, DELETE .../{upload_id}) are documented in the OpenAPI spec but deliberately have
    no MCP twin — they authenticate on a grant/session bearer credential, never
    an operator session. The legacy, unauthenticated POST /api/v1/file-retrieval stub (accepted a body-supplied agent_id, stored
    nothing) is removed; exactly one handler now serves file retrieval.
    ScheduleRunner's arming re-check (PR1.5a) is wired to a real
    authorization lookup: a schedule whose creator has since lost the required
    permission for its target plugin.action no longer fires, on both the
    auto and approval-gated paths. PluginConfigStore's secret column is now
    enrolled in the live KEK rotation/rewrap/status surface alongside the auth
    store's — an operator's /secrets/kek/rotate or /rewrap call re-wraps
    plugin secrets too, closing the gap docs/adr/3005-plugin-config-store.md
    recorded as open. (PR1.5c/PR1.6c)

  • Plugin config/secret plane + per-action kill switch. Born-on-Postgres PluginConfigStore
    (schema plugin_config_store) gives later default-off plugins a place to keep operator
    configuration, envelope-encrypted secrets (SecretCodec, enrolled in the live KEK-rotation
    surface), and a per-plugin or per-action kill switch. REST surface at
    /api/v1/plugin-config/*: list/get/set/delete config (PluginConfig:Read/Write/Delete),
    set/delete secret (PluginSecret:Write/Delete, write-only — no route ever returns a secret's
    plaintext), get/set kill switch (PluginConfig:Read/Write). Kill-switch evaluation is
    fail-closed: a degraded store reads as "disabled", never "enabled". Agent-side secret delivery
    is a deliberately open item — see docs/adr/3005-plugin-config-store.md. (PR1.5b)

  • Typed schedule parameters + arming re-check on every fire path
    (PR1.5a).
    POST /api/schedules now accepts an optional parameters
    object (string/number/boolean values only; nested objects/arrays, an
    empty/underscore-prefixed key, more than 32 keys, or a canonical
    serialization over 4KB are rejected with a 400). The stored form is
    canonical — keys sorted, so the same logical parameters always serialize
    identically regardless of the order the caller wrote them in — and reaches
    the scheduled dispatch exactly as a real
    std::unordered_map<std::string,std::string>, replacing the hardcoded
    empty "{}" ScheduleRunner previously sent on every fire. Parameters are
    set at creation only; there is no update route (delete and recreate to
    change them). Existing schedules migrate forward with the canonical empty
    object and are unaffected.
    Separately, ScheduleRunner::fire() now re-verifies the arming principal's
    authority (Deps::arming_check) BEFORE branching on approval, so the
    re-check covers both the direct-dispatch (approval_mode=="auto") path
    and the approval-gated path — not only the latter. The callback is
    fail-closed: unset, it denies the fire, audits the denial (principal +
    target plugin.action), counts it, and still advances the schedule so a
    permanently-denied schedule cannot spin. It sits in front of the existing
    approval-ticket flow (ApprovalManager / fire_with_approval) as an
    additional gate, not a replacement for it.

  • One-time upload grant + authenticated chunked artifact receive (CC-06 fix, server side). The
    server gains a new /api/v1/upload-grants (mint/list/revoke, operator-gated) + /api/v1/uploads
    (session open, chunked PUT, status/resume, commit, cancel) REST surface backed by a new
    born-Postgres UploadGrantStore, replacing the trust model of the old
    POST /api/v1/file-retrieval stub — which accepted a client-asserted agent_id and destination
    path with no real authentication and never durably wrote the uploaded bytes. An operator mints a
    single-use grant naming the agent/source/size/hash bounds; redeeming it (atomically, exactly
    once) opens an upload session whose own credential authenticates every chunk. Neither credential
    is ever stored in plaintext (SHA-256 digest only, one-time reveal at mint/session-open). See
    docs/adr/3004-artifact-blob-storage.md for the design.

  • Cross-fragment capability-catalogue drift gate (PR1.9). Two new hermetic tests keep the five per-plugin-group capability_decls/plugin_action_catalogue_*.hpp fragments plus core_dispatch_capabilities.hpp honest as a whole, since no single fragment's author can see the other five. tests/test_capability_catalogue_complete.py (python3 tests/test_capability_catalogue_complete.py) parses every plugin's actions() override and cross-references it against all six catalogue sources, naming the exact plugin.action when a plugin declares an action with no catalogue row, a fragment declares a row no plugin actually has, or the same plugin.action is declared by more than one source. tests/unit/server/test_capability_catalogue.cpp ([server][dispatch][capability]) composes a CommandCapabilityRegistry over all six sources and asserts the catalogue-wide invariants: every row's risk_tier is at or above its operation's floor, every securable/operation is one rbac_store.cpp actually seeds, every Destructive row is Irreversible, system_reserved is true only for core_dispatch_capabilities.hpp rows, classify() resolves every declared pair, and a locally-constructed duplicate span makes the registry report Ambiguous.

  • Command capability classification core (PR1.9a). New header-only server/core/src/command_capability.hpp / command_capability_parsers.hpp define the plugin.action classification vocabulary (DispatchClass, Mutability) and a CommandCapabilityRegistry that composes several independently-authored capability fragments and classifies a dispatch case-insensitively — an unknown plugin.action is always Unclassified, never a permissive default, and the same plugin.action declared by two fragments is Ambiguous, never first-wins. Also ships the frozen v1|<class>|<mutability>|<plan_hash_hex> dispatch-tag wire grammar (encode/decode) and an order-invariant compute_plan_hash. capability_decls/core_dispatch_capabilities.hpp declares the three system-initiated dispatches (tar.fleet_snapshot, __guard__.push_rules, asset_tags.sync). Not yet wired into a live dispatch path — a future PR consumes this registry from the dispatch chokepoint. authz_model.hpp additionally gains three new CapabilitySeed rows (PluginConfig, PluginSecret, UploadGrant), reusing only existing Operation enumerators. RbacStore::seed_defaults() seeds the same three securables into rbac.db — Administrator gets full CRUD via the existing loop, PlatformEngineer gets Read/Write/Delete on all three, and Operator gets Read on PluginConfig/UploadGrant only, never PluginSecret — closing the gap (peer finding PLAN-001) where a securable absent from the live seed cannot receive a default grant, denying even an Administrator on a fresh RBAC-enabled install.

  • Dispatch classification tag on the wire, with a gateway wire-capability handshake (CC-03). CommandRequest now carries dispatch_tag (field 9), the opaque v1|<class>|<mutability>|<plan_hash_hex> token the dispatch chokepoint uses to classify a command as read-only, mutating, or destructive. The field is mirrored into both of the Erlang gateway's vendored agent.proto copies and forwarded untouched on the command-fanout path, the same way the existing Guardian payload field is. Because gpb silently drops any field a gateway build's vendored proto doesn't declare, the gateway now advertises the literal command_dispatch_tag_v1 in StreamStatusNotification.wire_capabilities (a new field on the same connect-time message the gateway already sends) so the server can tell a gateway that would strip the tag apart from one that carries it end to end.

  • Weekly leaked test-database sweep (#1367). The cache-prune.yml maintenance run now sweeps leaked yuzu_test_* databases off every persistent self-hosted CI Postgres instance (scripts/ci/sweep-test-databases.sh): epoch-named databases age by the same >6h server-clock rule as the in-suite sweeper, and names the epoch sweeper can never parse (pre-epoch format, implausible clock stamps) are reclaimed once the datdir's PG_VERSION mtime exceeds 7 days with zero active backends — closing the "idle box never sweeps" and "permanently unsweepable names" gaps. The same round fixed the Windows prune job's silent no-op (find resolved to System32's, not coreutils') and pinned clean: false on the new checkout steps so the weekly job can't wipe the runners' warm build trees.

  • macOS crash reports now carry the application version. .ips crash and hang reports route the app's CFBundleShortVersionString (from the report header app_version, falling back to the body bundleInfo) through the shared canon_version normaliser into the DEX signal's version field, so macOS crashes join the per-(app, version) reliability identity the same way Windows crashes do. Unversioned binaries (unsigned CLI tools) report an empty version, exactly as store/packaged apps already do on...

Read more

Yuzu v0.13.0

Choose a tag to compare

@github-actions github-actions released this 11 Jul 14:43

Warning

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

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

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


Added

  • Recurring instruction schedules now actually fire (#1191). ScheduleEngine::evaluate_due/advance_schedule had no production caller — schedules persisted and listed but never dispatched. A new ScheduleRunner poller (the PolicyEvaluator/PreflightRunner shape, 30s tick, joined before the stores) closes the gap: due schedules fire through the same tracked dispatch path as operator-initiated commands, the approval gate is never bypassed (requires_approval OR definition approval_mode != "auto", one ticket per occurrence, one-approval == one-run via the occurrence anchor), and every outcome is audited (instruction.schedule_fired, instruction.approval_required) and counted (yuzu_schedule_{fires,fire_failures,approvals_submitted,tick_errors}_total). Hardened against adversarial review before merge: POST /api/schedules requires BOTH Schedule:Write and Execution:Execute — a created schedule is a fleet-wide command-dispatch primitive with no further permission check at fire time — and re-enabling a disabled schedule requires Execution:Execute too (disabling never does, so a runaway schedule can always be stopped); delete and enable/disable are owner-scoped (created_by); one approval ticket is now scoped to the single schedule occurrence that requested it (approvals.schedule_id), so two schedules sharing the same (creator, definition, scope) can no longer settle on each other's ticket.
  • MCP agentic write surface + approval tickets (#289, R2). Five MCP write tools
    are now dispatched: set_tag, delete_tag, approve_request, reject_request,
    and quarantine_device (records the quarantine and dispatches live isolation).
    Approval-gated tools use a ticket-then-recall flow: the first call returns
    kApprovalRequired (-32006) with approval_id + status_url; after a second
    principal approves, the caller re-invokes with the approval_id and the ticket is
    consumed exactly once (args-bound, replay-rejected). Approval ids are CSPRNG-drawn;
    the mint is deduplicated so a token cannot flood the shared pending-approval queue.
    Review-round hardening (PR #1796): quarantine_device is Security:Execute on BOTH
    transports and the supervised approval-policy rule moved with it — closing a mismatch
    that let a supervised token quarantine via REST POST /api/v1/quarantine with no
    approval (the REST POST/DELETE routes now mirror-deny, #520); the three
    device-targeted write tools (set_tag/delete_tag/quarantine_device) authorize
    through the per-device management-group scope gate (require_scoped_permission), so
    a group-confined operator cannot tag or isolate out-of-scope devices;
    approved-but-unconsumed tickets expire after 7 days (a forgotten approval is no
    longer a forever-valid capability); the approvals store records consumed_by
    (SOC 2 CC7.2 evidence chain: submitted_by → reviewed_by → consumed_by); and the
    device-targeted ticket-mint audit rows carry agent_id= so SIEM can filter pending
    isolation requests by endpoint.
  • A2 discovery surface (Issue 17.1, #1794). GET /api/v1/discover/{permissions, instructions,routes,scope-kinds,plugins} plus five mirrored read-only MCP tools let
    an agentic worker enumerate RBAC permissions, instruction schemas, REST routes,
    scope-DSL kinds, and plugin actions from the live server (ETag + 304, A4-shaped
    degraded paths). REST and MCP share one builder per catalog (cannot drift).
  • Agentic-worker self-orientation (R2 follow-up). discover_plugins (REST + MCP,
    now catalog version: 2) enriches each action with its parameter_schema inline
    when the action has a published InstructionDefinition (joined on plugin+action), and
    reports actions_enriched_with_schema — so a worker learns how to call an action,
    not just that it exists, from one request. The execute_instruction and
    discover_plugins tool descriptions now spell out the async dispatch→poll pattern and
    point at the yuzu://operating-model resource. Measured effect: an LLM operator
    driving the fleet with no hand-fed action list made zero blind parameter guesses.
  • A4 error-envelope completion (R2). GET /api/v1/approvals/{id} (the approval
    status_url target); the auth_routes denial shapes are unified into one A4
    envelope carrying correlation_id and the missing securable_type:operation; the
    ~156 legacy error_json sites in rest_api_v1.cpp now emit the A4 envelope.
    Breaking — many of those /api/v1 errors were previously {"error":"<string>"}
    and are now the nested A4 object {"error":{"code","message","correlation_id",…}};
    a client that read error as a string will break. Migrate to error.code/
    error.message. See docs/user-manual/upgrading.md.
  • Installed-software inventory gains package-manager fields (blob contract v2, ADR-0016).
    Every row in GET /api/v1/inventory/software and the query_installed_software MCP tool
    now carries kind (package|app), ecosystem (rpm|deb|apk|pacman|windows|macos|homebrew),
    epoch, release, arch, signature_status (rpm only, from stored rpm header tags —
    never a live rpm -K verification), distro_id, and distro_version, alongside the
    original name/version/publisher/install_date. A field the ecosystem does not store is
    "", never synthesised — see docs/user-manual/inventory.md for the full per-ecosystem
    matrix. Collection is a new installed_apps plugin action, list_inventory; the
    operator-facing list/query/list_per_user actions are unchanged. New apk
    (Alpine) package enumeration on the sync path. Breaking — data shift on Linux
    rpm fleets:
    publisher now reflects the rpm PACKAGER tag (was VENDOR), and version
    is upstream-only with the release moved to its own column (was fused
    VERSION-RELEASE). A saved query/dashboard filter against the old publisher or
    fused version shape on rpm hosts will silently stop matching post-upgrade — see
    docs/user-manual/inventory.md before relying on either field in existing
    automation. No wire-protocol or gateway change (the fields ride inside the
    existing opaque sync blob). Upgrade note: deploy the server before agents; each
    agent's first post-upgrade sync triggers one expected full resend (need_full),
    phase-spread across the daily sync window, self-healing.
  • JIT elevation: OIDC amr-asserted MFA satisfies the second-factor
    requirement.
    An OIDC operator can now activate POST /api/v1/elevate
    when their SSO session was authenticated with IdP MFA — asserted via the
    OIDC amr claim and seeded into Session::mfa_verified_at at
    /auth/callback (the same mechanism OIDC login step-up already used).
    Closes the v1 limitation tracked in
    docs/security-reviews/jit-elevation-2026-06-30.md. Eligibility and MFA
    status are both keyed on a users table row, which OIDC login does not
    create — a federated-only identity needs one provisioned first (e.g.
    POST /api/v1/users). The mandatory-MFA gate branches EXPLICITLY on
    identity source: an OIDC session's only acceptable factor is a seeded
    proof (mfa_verified_at != epoch, never merely auth_source == "oidc",
    security-F1) with the toggle on, and it can never fall through to a
    local namesake account's TOTP enrollment (a hardening-round fix — the first
    cut's single-flag guard allowed exactly that fallthrough, mislabeling the
    audit trail). A single-factor (no-amr) SSO session is hard-denied with
    its own distinct reason; a stale seeded proof still falls through to the
    existing step-up freshness challenge rather than a silent grant. New toggle
    --jit-oidc-amr-elevation / YUZU_JIT_OIDC_AMR_ELEVATION (default true;
    --no-jit-oidc-amr-elevation disables JIT elevation for OIDC sessions
    entirely — they cannot fall back to a local TOTP step-up, since their
    step-up challenge is re-SSO, not a TOTP code). A one-time INFO log line
    announces the posture at boot when OIDC is configured. The
    role.elevation.granted audit detail records the factor source
    (duration_secs=<n> mfa=<oidc_amr|local_totp> expires_at=<rfc3339> justification=<text>
    — the machine-parsed mfa= and expires_at= tokens are placed before the
    free-text justification= field so a crafted justification can't forge either).
  • JIT admin elevation follow-ups: passive-lapse audit + absolute expires_at (SOC 2
    CC6.3/CC6.6).
    Closes the two residual risks tracked in
    docs/security-reviews/jit-elevation-2026-06-30.md. (A) A JIT elevation window that
    lapses PASSIVELY (no manual POST /api/v1/elevate/revoke) is now audited as
    role.elevation.expired — emitted LAZILY (no new background thread) by
    AuthManager::reap_expired_elevation, called from the AuthRoutes::resolve_session
    cookie chokepoint on the...
Read more