Release notes

All notable changes to this project are documented in this file.

The format is based on Keep a Changelog.

Unreleased

[0.2.1] - 2026-11-11

Upgrading from v0.2.0: read the upgrade notes below; there is no migration and no new setting. Changes to security controls are in the section Security.

Upgrade notes

  • From v0.2.0 only the version changes. There is no database migration (the schema and its compatibility level are those of v0.2.0, so replicas of v0.2.0 and v0.2.1 can run side by side during a rolling update, and helm rollback to v0.2.0 works), no new or changed setting and no new or changed value of the two charts; the installers, the systemd units and myriarch-updater work as in v0.2.0. Update the server image or chart (helm upgrade myriarch oci://ghcr.io/myriarch/charts/myriarch --version 0.2.1 -n myriarch -f <your values file>) and, if you run it, the collector chart (--version 0.2.1): its pods restart once, because the chart version and the updater image are part of the pod template, while the collector and supervisor images keep the pins of v0.2.0. Hosts keep working with the updater of v0.2.0; run the installer of v0.2.1 on a host again to give it the updater that is built with Go 1.27.4 (see Security). Building from a checkout needs Go 1.27.4, the version in go.mod.
  • Copied install commands name the version of the server. A server serves only the installer bundles of the release it runs. After the update it fetches those of v0.2.1 from install.bundleSourceBaseUrl, so a mirror in an air gap needs checksums.txt, checksums.txt.sigstore.json and the updater archives of the platforms of install.platforms under <base>/v0.2.1/ first, and a command that was copied before the update fails at the download with HTTP 404. Copy the command again from the dialog Add host once every replica runs v0.2.1.
  • From v0.1.0, follow Upgrade from v0.1.0 to v0.2.0: it applies to v0.2.1 unchanged, with 0.2.1 where its commands name 0.2.0.

Added

  • ci/check-stale-versions.py, run by ci/check-versions.sh: a text that says “planned for” or “until” a release that exists, and a placeholder page planned for such a release, fail the check of the pull request that prepares the release.
  • Website: OpAMP explained. The concept page OpAMP, the Supervisor and the Collector explains what OpAMP is and the parts of an OpAMP setup (server, Supervisor, collector, OpAMP Bridge and myriarch-updater). It shows how a configuration reaches a collector, how rollback works on the host and for a whole rollout, what the Supervisor cannot do and the two ways to run managed collectors on Kubernetes, with the dated sources of the upstream notes. The FAQ answers eight new questions: what OpAMP is, what the Supervisor can and cannot do, how the OpAMP extension differs from the Supervisor, which collector distribution myriarch manages, whether collectors can be updated without internet access, whether Ansible AWX or a pipeline can keep installing collectors, how to roll back a bad configuration, and how myriarch relates to the OpenTelemetry Operator and its OpAMP Bridge. The comparison has sections on the Operator and on existing automation. The page “What is Myriarch?” shows the key terms of llms.txt and the hosting model.

Changed

  • The message of the server for an OpAMP Bridge that is held back from a version rollout no longer names an executor that does not exist.
  • Website: findability. The landing page says in its first lines what myriarch is (an open-source OpAMP server and fleet manager for OpenTelemetry Collectors) and which gap of the OpAMP Supervisor it closes. The same sentence opens llms.txt, the FAQ, the project pages, the web app manifest, the README and the description of the home page. More pages have a search title of their own (website/data/seo_titles.yaml), and the main menu links the overview page; below a width of 992 px it leaves out the entry API reference, which the documentation links, so that the light/dark switch stays in view. The documentation index, the tutorials and the how-to guides link the overview, the comparison, the FAQ and the guide for many collectors. The Open Graph and Twitter titles of a page are its search title without the suffix, and the cards give the type, the size and a text alternative of the image. A page states a publication date only when it shows that date. The placeholder pages of planned guides carry noindex, follow and are left out of the sitemap; check-indexing.py and check-seo.py check both. llms.txt links more operator pages and states that the collector numbers are design targets. The production alias myriarch.vercel.app redirects to https://myriarch.dev/, and the release notes link the documentation on the website.
  • Social images: a tagline that the documentation backs. The Open Graph image of the website, the image prepared for the GitHub social preview (assets/brand/social-preview.png) and the description of the web UI now say “A GitOps-governed fleet manager for OpenTelemetry Collectors, built on OpAMP.” The old line, “Command ten thousand OpenTelemetry Collectors from one GitOps-governed control plane.”, read as a capacity claim, while the documentation gives around 1,000 collectors, with headroom up to 5,000, as design targets. ADR-0050 records the change and supersedes the tagline item and the explanation of the name in ADR-0015; the name keeps its etymology.

Fixed

  • The charts on Artifact Hub. Artifact Hub shows the README of the chart package, and its links were relative to the repository (../../../docs/operations/install-helm.md, values.schema.json), so they did not resolve there. The release now writes the links of the packaged README of both charts as links to the repository at the release tag (https://github.com/myriarch/myriarch/blob/v0.2.1/docs/operations/install-helm.md, tree for a directory, the anchor kept) and stops the packaging when a link names a path that the tagged commit does not have; verify-chart refuses a chart whose README still has a relative link. The READMEs in the repository keep their relative links, which are right on GitHub and on the website. Both charts have more keywords for the search: opamp-server, opentelemetry-collector, gitops and observability for the server chart, and opamp-supervisor, observability and kubernetes for the collector chart.

Security

  • Windows files of v0.2.1 are unsigned. The SignPath signing is not set up yet, so v0.2.1 ships myriarch-updater.exe, myrctl.exe and the two PowerShell scripts without Authenticode signatures, under a reviewed exception for exactly that tag (WINDOWS_UNSIGNED_FINAL_TAG in deploy/release/windows-signing.env, ADR-0034). Verify them with cosign before you run them (the security model).
  • Go 1.27.4. The Go toolchain of the main module and the Go build images (myriarchd, myriarch-sim, the deprecated combined collector image) move from 1.27.3 to 1.27.4, which fixes GO-2026-6859 in net/http reported by govulncheck.

Known issues

  • A nightly browser test of updates from the UI fails at one check. The job ui-updates of the e2e workflow does not always see the short health-check phase of an update before the update ends. The other checks of the test pass. The job is not part of ci-ok or of the tag guard.

[0.2.0] - 2026-11-06

Upgrading from v0.1.0: read the upgrade notes below first, and Upgrade from v0.1.0 to v0.2.0. Changes to security controls are in the section Security.

Upgrade notes

  • Back up the database before you upgrade from v0.1.0; going back to v0.1.0 means restoring that backup. The upgrade applies new migrations (the first is 00006_schema_compat.sql, expand only). v0.1.0 does not check the schema and its migration library accepts a database with higher versions, so it would start on a migrated database without a warning. A rollback to v0.1.0 is not supported (ADR-0036). To prepare, take a backup of the PostgreSQL database (pg_dump or a snapshot of the managed database) before helm upgrade or before you start the new image. To go back, restore that backup and start v0.1.0 against it.
  • The server can refuse to start because of the database schema. The database records a compatibility level, and myriarchd serve compares it with its own migrations. A schema that is incompatible with the binary is refused with an error that names the schema version and the levels, whatever database.migrateOnStart says. With database.migrateOnStart: false, a schema that lacks migrations of the binary is refused at once and the error names myriarchd migrate, where v0.1.0 started and failed later. A newer schema that is still compatible only logs a warning. If you run with database.migrateOnStart: false, run myriarchd migrate before you update the deployment; with the default true, nothing changes for you.
  • myriarchd migrate reads only the database settings and prints other lines. The public URL, the login path and the secret files of other features are no longer needed or checked, and other sections of the configuration file are ignored; a file with database.dsn or database.dsnFile alone, or MYRIARCH_DATABASE_* variables alone, is enough. The output schema version: N is replaced by schema version before: N, schema version after: N and compatibility level: N. A script that reads the old line has to read schema version after:.
  • Helm chart: strict values schema. helm install, upgrade, template and lint now validate the values against values.schema.json and refuse what the chart used to ignore or to accept without a word: a key that the chart does not know (a misspelled setting, also inside config), a value of the wrong type or in the wrong form (a number where the chart needs a string, a string such as "false" where it needs a boolean, a duration written as an integer such as config.server.shutdownTimeout: 30 or with a unit that Go does not know such as 30d, an image digest without sha256:, a registry with a scheme) and a secret as a value. The schema has no key for config.database.dsn, config.auth.oidc.clientSecret, config.gitops.webhookSecret, config.notifications.channels[].url or an enrollment token (enrollmentTokens[].token), and a Secret reference such as enrollmentTokens[].key must not have the form myre_...; use the *File settings and Secrets. config.auth.bootstrapAdmin, config.opamp.enrollmentTokens, config.dev and config.auth.devLogin were refused when the chart rendered and are now refused by the schema. Before you upgrade, run helm lint (or helm template) with your values file and the chart of this release and fix what it reports; the message names the path of the value. A release that was installed with --set or upgraded with --reuse-values has no values file: helm get values <release> -n <namespace> -o yaml > current-values.yaml writes its values. See Install with Helm.
  • Helm chart: the registry of the image is a value of its own. image.registry (default ghcr.io) holds the registry host, image.repository (default myriarch/myriarch) the repository without it, and global.imageRegistry replaces the registry, for example with a mirror. A repository that starts with a registry host (registry.example.com/myriarch/myriarch, the form of the values of v0.1.0, or localhost:5001/myriarch) is used unchanged, so values that set the full repository and the defaults render the same image as before. A repository without a registry host now takes image.registry: if you relied on the default registry of your container runtime for such a repository, set image.registry: "" or put the host into the repository. The schema refuses a registry with a scheme or a path and a repository with a tag, a digest or upper case letters, which Helm rendered into an invalid image reference before.
  • A stalled APPLYING is offered again. When an agent reports APPLYING for the desired configuration on a connection and the status stays unchanged for opamp.staleApplyingAfter (new setting, default 90 seconds), myriarchd offers the configuration once more on that connection, once per connection and never for a hash marked failed. The agent event config_offered carries the reason stale_applying. Before, a target in this state waited until the applyTimeout of its rollout policy, 10 minutes for configuration by default; a supervisor that restarts while it applies reports the stored APPLYING again and nothing ends it. A supervisor answers the repeated offer of an unchanged configuration with APPLIED and does not restart the collector. What to do: nothing. To keep the old behaviour set opamp.staleApplyingAfter: 0; keep a value above the config_apply_timeout of your supervisors (30 seconds in the shipped templates), so that a supervisor that is only slow is not offered the configuration again. A negative value is refused at startup.
  • The warning bridge_selected can fail a strict GitOps check. myrctl validate and the GitOps sync warn when the selector of a fleet also matches an OpAMP Bridge agent of the OpenTelemetry Operator and the fleet does not set the new field spec.bridge.manage. An empty selector triggers it, and so does a selector made only of NotIn and DoesNotExist conditions; so does a sample agent of the repository that is a Bridge and matches such a fleet. A CI that runs myrctl validate --strict fails on the warning; without --strict the exit code stays 0. What to do: narrow the selector with a positive condition on an attribute that a Bridge does not report, for example os.type or site.type ({key: os.type, operator: Exists} replaces an empty selector that is meant to match every collector), or set spec.bridge.manage: true on a fleet that is meant to configure Bridges in standalone mode. Fleets with a positive condition (matchAttributes, In, Exists or Glob) on os.type, a site.* attribute or labels.* are not affected. A sample agent that is a Bridge is no longer rendered and does not have to match a fleet.
  • The installers stop at a package of the upstream project. install-linux.sh and install-windows.ps1 stop with conflicting_install, before they change anything, when a package of the upstream project would run a collector next to the one that myriarch manages: on Windows a service opampsupervisor (supervisor MSI) or otelcol-contrib (contrib MSI), in any state; on Linux the unit otelcol-contrib.service or opampsupervisor.service of a deb or rpm package, active or enabled. The installers of v0.1.0 went on and left two collectors on the host, and a re-run on a host that was set up next to such a package stops now as well. Nothing is removed. What to do: remove the package as the message and the table “When the installer stops” in Connect a host describe, and run the installer again. A package whose units neither run nor are enabled (the rpm packages since 0.157.0 start nothing) does not stop the installer.
  • Helm chart: a test Pod, and a source for it in the NetworkPolicy. The chart renders the Pod of helm test (templates/tests/test-connection.yaml, the Helm hook test, value tests.enabled, default true). Helm creates it only while helm test runs, so an installation or an upgrade creates no Pod. Tools that render hooks as ordinary resources create it as a normal Pod that runs once and ends: helm template | kubectl apply does (add --skip-tests), and tests.enabled: false removes it. With networkPolicy.ingress.allowSameNamespace: false and without config.server.trustedProxies and config.opamp.clientCertHeader, the NetworkPolicy gets one more source for port 8080, a pod selector with the labels of the test Pod (app.kubernetes.io/component: test), which every pod of the namespace that carries these labels can use. To keep the policy of v0.1.0, in which no pod of the namespace reaches port 8080 unless networkPolicy.ingress.from names it, set tests.enabled: false. With trustedProxies or clientCertHeader set the chart adds no rule, and helm test can connect only if networkPolicy.ingress.from names the test Pod. The default allowSameNamespace: true already admits every pod of the namespace. helm upgrade --reuse-values renders the chart with the values of the installed release in place of its own defaults, and the values of v0.1.0 have neither tests nor prometheusRule: the chart then renders as v0.1.0 did, without the test Pod, without the NetworkPolicy source for it and without the PrometheusRule, and --set tests.enabled=true on top of them gives a Pod without requests and limits. To get the defaults of the new chart, upgrade with --reset-then-reuse-values or with a values file (helm get values <release> -n <namespace> -o yaml). See Install with Helm.
  • install.grantTtl must not exceed artifacts.downloadTokenTtl. The new setting install.grantTtl (default 1 hour) is the lifetime of the install grants, and production refuses a value above artifacts.downloadTokenTtl. A configuration of v0.1.0 that shortened artifacts.downloadTokenTtl below one hour and does not know the new setting stays valid: when install.grantTtl is left out (in the file and in MYRIARCH_INSTALL_GRANTTTL), its default is the shorter of one hour and artifacts.downloadTokenTtl. A value that you set yourself is checked as written. Set install.grantTtl to at most artifacts.downloadTokenTtl, or turn the installation from the server off with install.enabled: false, and run myriarchd config validate before you update.
  • A server that runs a release fetches the installer bundles of its own release. From v0.2.0, every replica of a server that runs a release (not a build from a Git checkout) fetches checksums.txt, its Sigstore bundle and the updater archives of that release for the platforms of install.platforms (three by default) from install.bundleSourceBaseUrl, shortly after the start (15 seconds) and again when somebody renders the install commands or asks for a bundle. The default source is the GitHub releases of the project (github.com/myriarch/myriarch), so this is a new outbound connection of the server, through outbound.allowedCidrs, artifacts.proxyUrl and artifacts.allowedRedirectHosts. A server that cannot reach the source logs a warning and shows the bundles as failed; nothing else is affected, and the commands cannot be rendered until it can. In an air gap, point install.bundleSourceBaseUrl to an internal mirror with the layout <base>/v<version>/<asset>, or set install.enabled: false, which keeps the surface of v0.1.0: the routes answer 404 and nothing is fetched. The bundles take about 25 MB of artifacts.dir for each release and replica, outside artifacts.maxCacheBytes; the archives of an earlier version are removed after a day. See Server behaviour.
  • Enrollment tokens are install credentials now. With the default install.enabled: true, an enrollment token also authorizes two routes that need no session, POST /api/v1/install/plan and GET /api/v1/install/bundles/{filename} (also where opamp.requireEnrollmentToken is off), and, through the install grants of a plan, the download of collector and supervisor with their checksums and signatures. Its holder can ask for plans for the platforms of install.platforms and so make the server prepare every release that the frame of its fleet allows. Handle the tokens as before and treat them as fleet credentials: one token for each channel or region, and revoke one that may have leaked, which stops its grants at once. Set install.enabled: false if the hosts of your sites are installed on the manual path only. See Host installation from the server.
  • OpAMP Bridge agents are a kind of their own, and the server holds back what could change their collector resources. Migration 00007_agent_kind_bridge.sql (expand only) widens the kinds of agents.kind by bridge. An agent that reports the service.name io.opentelemetry.operator-opamp-bridge or the attribute opentelemetry.io/opamp.bridge.mode is stored as kind bridge with its next description message. Such an agent receives remote configuration only from a fleet with spec.bridge.manage: true, and only in standalone mode; it is never sent a restart command (the REST action answers 412) and never a target of a version rollout. If a Bridge of the OpenTelemetry Operator was connected before, it was treated as a collector so far: check that no fleet relies on configuring it, and scale myriarchd to one replica for the update, because replicas of an earlier release classify a Bridge as a collector until all replicas run this version.
  • Fleet assignments are written as a compare-and-set; a rolling update from v0.1.0 needs nothing special. The assignment service writes an agent’s assignment only if the assignment in the database is still the one it read, and records the fleet change and resets the desired state (and with it failed_config_hash) only when that write took effect. This needs no migration and no new column, so v0.1.0 and the new release can run side by side during a rolling update. Replicas of v0.1.0 still write unconditionally: while they run, two replicas can still record one fleet change twice, as before. The protection is complete when all replicas run this version.
  • install-linux.sh no longer requires --collector-version. Without --server and without the option, the installer installs the version that its release was tested with (the OTELCOL_VERSION of ci/versions.env) and says so; --collector-version latest asks GitHub for the newest release, on request only, because that version is not the one that was tested. Before, the missing option stopped the run with option_missing. With --server the version comes from the plan. install-windows.ps1 still requires -CollectorVersion without -Server. A script that relied on the error now installs the tested version: pass the version of your fleet explicitly, or use --server.
  • The collector chart asks for more memory by default. resources.limits.memory is now 1Gi (was 512Mi), resources.requests.memory is 512Mi (was 256Mi) and initResources.limits.memory is 1Gi (was 256Mi). With the old limit the main container was OOM-killed in a chart installation test; with 1Gi it ran without restarts. An installation that sets its own resources or initResources keeps them. The memory_limiter of the blueprints takes its limit as a percentage of the container limit, so it follows the new value without a change in Git. The init containers run one after the other and before the main container, so the request of the pod does not grow; check that the nodes have room for the higher request of the main container before you upgrade a DaemonSet.
  • The collector chart always runs a health_check extension on 0.0.0.0:13133. The chart mounts a small local configuration (healthCheck.enabled, on by default) that the supervisor merges before the configuration from the server. The server’s values win on conflicts, but service.extensions is concatenated, so a fleet cannot remove health_check. A fleet configuration that moves extensions.health_check.endpoint to localhost or another port makes the pod not Ready; keep it on the pod IP. Set healthCheck.enabled: false to go back to the old behavior, where the pod is Ready only if the fleet configuration has health_check on the pod IP (or while the startup fallback runs).
  • The upgrade from v0.1.0 to v0.2.0 is described in one place: Upgrade from v0.1.0 to v0.2.0. It covers the two expand-only migrations and the rolling update, the new and changed settings, the Helm chart, the installers, the GitOps warning bridge_selected and the deprecated Kubernetes collector files.

Added

  • Bounded leader failover (ha.leaderSessionTimeout). After a hard failure of the leader’s node (no TCP FIN), PostgreSQL held the advisory lock until it noticed the dead session, which can take hours with the operating system’s TCP defaults, and there was no leader and no rollout until then. The lock session and the event bus listener session now send tcp_keepalives_idle, tcp_keepalives_interval, tcp_keepalives_count and tcp_user_timeout derived from the new setting ha.leaderSessionTimeout (default 30s, 10s to 10m; MYRIARCH_HA_LEADERSESSIONTIMEOUT), so a second replica becomes leader within that time in total: the setting is split into the time the server needs to drop the dead session and the retry of the followers (every 5 seconds, or a sixth of the setting if shorter). The leader gives up leadership itself after half of the timeout without an answer on its lock session, before another replica can take over, and asks the server to end that session. Both sessions now use a small pool of their own instead of a connection of the main pool, so a database with a tight max_connections sees up to two more connections per replica. See High availability for the failover time and for the sizing of server.shutdownTimeout and terminationGracePeriodSeconds, whose worst case covers both drain phases and the flush (90 seconds with the defaults; the chart default of 60 covers a normal shutdown).
  • Database test matrix. The Go tests run in CI on an embedded PostgreSQL with TLS (sslmode=verify-full) and with a collation other than C, next to the existing run against the PostgreSQL service. Locally PGTEST_TLS=1 and PGTEST_COLLATION=<locale> select the same environments (development guide). New tests apply every migration to an empty database and upgrade a database with the schema of v0.1.0 (whose migration files are pinned by hash).
  • myriarch-updater install-self <dir> and myriarch-updater copy <src> <dst>. Two commands for init containers: the upstream collector image is FROM scratch and has no shell or cp. install-self copies the running binary to <dir>/myriarch-updater with mode 0755; copy copies one regular file, keeps its execute bit and accepts a directory as destination. Both are atomic (temporary file, sync, rename), need absolute paths, refuse a symlink (also as destination directory), a directory or a device as destination, refuse a named pipe or device as source and make no network access (container mode).
  • Collector chart deploy/helm/myriarch-collector. Runs OpenTelemetry Collectors that the server manages as a StatefulSet (a gateway, a PVC per replica for the identity) or a DaemonSet (a node agent, persistence.hostPath as an option), with the unchanged upstream images: an init container with myriarch-updater runs install-self /shared, an init container with otel/opentelemetry-collector-contrib runs /shared/myriarch-updater copy /otelcol-contrib /shared/, and the main container is otel/opentelemetry-collector-opampsupervisor. Every image has a tag and an optional digest; the tags and digests of the two upstream images are those of ci/versions.env. The values cover the enrollment token (an existing Secret, never a value), a CA bundle, the site attributes, a wss:// endpoint (ws:// renders only with server.allowInsecureTransport=true), resources, tolerations and a NetworkPolicy. The values schema is strict and is converted from values.schema.source.yaml by make docs-gen, like that of the server chart, and so is the values table of the README. ci/lint-helm.sh lints the chart in both modes, in container mode and in the OpenShift profile and packages it, and ci/helm-conform.sh (through the new ci/helm-conform-collector.sh) renders it for every Kubernetes version of the gate, validates the manifests with kubeconform and asserts the contract, including the guards that make rendering fail. Install the collector with Helm.
  • Collector chart: container mode as an option. containerMode.enabled makes myriarch-updater PID 1 of the main container, adds the second volumeClaimTemplate binaries, probes on :13135 and a terminationGracePeriodSeconds of at least 360 seconds, and needs the Sigstore trusted root as a ConfigMap unless the verification is switched off on purpose. It is available for the StatefulSet only.
  • Collector chart: signed OCI artifact. From v0.2.0 every release also publishes oci://ghcr.io/myriarch/charts/myriarch-collector, packaged with the digest of the signed updater image, signed keyless and attested like the server chart, and verified before publication.
  • Schema compatibility level. Migration 00006_schema_compat.sql adds the table schema_compat with one row. Its column min_binary_schema is the lowest schema level (the highest migration number a binary embeds) that a binary needs to run on the database. It starts at 3 and never exceeds the schema version. From 00006 on, the header of every migration states whether it is expand only (the level stays) or raises the level, a migration that raises it sets the value in Up and restores the previous one in Down, and tests check the statements against the effect (ADR-0036, development guide).
  • myriarchd migrate --check changes nothing: it prints the schema version, the compatibility level, the schema level of the binary and a status (current, pending, newer or incompatible) and tells the state by its exit code: 0 ready, 3 migrations pending, 4 incompatible, 1 error. Without --check, an incompatible schema is refused with exit code 4 as well.
  • Helm chart: values schema. deploy/helm/myriarch/values.schema.json (JSON Schema draft 2020-12) validates the values of the chart. Every object that the chart defines is closed, the Kubernetes objects that it passes on (such as volumes, security contexts, affinity terms and Gateway API references) are open, and the block config has the keys, types, nesting and descriptions of internal/config. A parent chart that includes the chart as a dependency keeps working: global and the key enabled of the usual condition: myriarch.enabled are accepted. make docs-gen generates the file from the hand-written values.schema.source.yaml and the configuration struct (a new generator in internal/refdoc); ci/check-docs-gen.sh fails on drift, and the unit tests fail on a value that values.yaml or a template uses and the schema lacks and on a setting of the configuration that is named like a secret and can hold text.
  • Helm chart: image reference. image.registry and global.imageRegistry (see the upgrade notes). podDisruptionBudget.maxUnavailable, which the template already read, is now a documented value.
  • Helm chart: signed OCI artifact. From v0.2.0 every release publishes the chart as oci://ghcr.io/myriarch/charts/myriarch with the version of the release as its version and appVersion. The release workflow (new jobs chart, push-chart and verify-chart) writes the digest of the server image into image.digest of the packaged chart, so the chart references the image by digest only and its keyless signature and provenance attestation cover the image that is pulled; it checks with helm template (default values and the OpenShift profile) that every image of the rendering is exactly that digest before it pushes. Like the images, the chart is pushed by digest without a tag, verified, and tagged with its version only by finalize after the second approval, which also waits for the chart package to be public. ci/release/chart-package.sh builds and checks the package without a network or a real registry (ci/tests/chart-package-test.sh); helm joins the signing tool set but runs only in the jobs chart and verify-chart. The package charts/myriarch has to be made public by an organization owner once, like the image packages. Install and verify: Install with Helm, Verify a release.
  • Helm chart: generated table of the values. The README of the chart lists every value with its type, default and description, generated from values.schema.json and values.yaml by make docs-gen; ci/check-docs-gen.sh (gate docs) fails when the table is out of date.
  • Helm chart: Artifact Hub metadata. Chart.yaml has the icon, the maintainers (the project address), the category, links to the documentation, the sources, the issues and the security policy, artifacthub.io/operator and a placeholder for the images that the release job completes with the digest.
  • Release: Artifact Hub metadata. The workflow .github/workflows/artifacthub.yml, which the maintainer starts by hand on the release tag, uploads the repository metadata file artifacthub-repo.yml (one key, repositoryID) of the server chart as an OCI artifact under the tag artifacthub.io of ghcr.io/myriarch/charts/myriarch, so that Artifact Hub can show the chart as “Verified publisher”. The job needs the approval in the environment release, refuses to run before the chart is published, writes only that tag and uses the pinned crane. verify-chart accepts the tag artifacthub.io only with the Artifact Hub media types. See Releasing.
  • Stale APPLYING. The setting opamp.staleApplyingAfter (default 90 seconds, 0 turns it off), the counter myriarch_opamp_config_reoffers_total (label reason, value stale_applying) and the reason stale_applying in the details of the agent event config_offered. The rule is described in Server behaviour.
  • spec.bridge.manage. The new field of a Fleet (default false) opts the fleet in to configure OpAMP Bridge agents. The opt-in applies to Bridges in standalone mode only. The new warning bridge_selected names selectors that also match a Bridge. The REST API returns the block as bridge of a fleet when it is set. See Fleets and the schema reference.
  • Dependency review. Pull requests and merge groups run a dependency review (dependency-review in ci.yml, a member of ci-ok): actions/dependency-review-action compares the dependency graph with the base and fails when a dependency that the change adds or updates, in the runtime scope, carries a license outside the CNCF allowlist (and Apache-2.0) or has a known advisory of high or critical severity. The review checks what the dependency graph of GitHub reports for the change with the scope runtime; which ecosystems (Go, npm, Python, GitHub Actions) that covers follows the dependency graph and is to be confirmed by the first runs. It holds the read-only token, writes no comment and asks no third party. ci/license-allowlist.txt is the source of both license lists: its list release is the binding allowlist of ci/check-release-licenses.sh for the Go packages of the release builds, which is as strict as before (the npm packages of the embedded web UI are checked against a license list by the review only; tslib under 0BSD ships in it today), and all its entries are the allow-licenses of .github/dependency-review-config.yml; ci/tests/license-allowlist-test.sh keeps the two in step and refuses an exception without a reason. The configuration records an exception, with its reason, for each of the 80 packages of the current dependency set that the review would otherwise report (tools, development trees, the brand generator, packages with combined permissive licenses). pkg:npm/elkjs (EPL-2.0, a dependency of Mermaid) has none until it is decided whether the website needs a notice for it, so a change that adds it or changes its resolved version fails the review. See ADR-0038 and “Dependency review” in docs/development.md.
  • Vulnerability reports in code scanning. The workflow vulncheck.yml (pushes to main, daily, by hand) reports govulncheck to the code scanning alerts as SARIF: the code of main built like a release and with the build tag devpg (ci/vulncheck.sh --sarif DIR) and the programs of the latest stable release (ci/vulncheck-binaries.sh, binary mode over the 16 archives). The scan of the binaries starts only after the keyless signature of checksums.txt and the SHA-256 of every archive were verified, and it reads out only the program of each archive. The release binaries are stripped (-s -w), and govulncheck cannot read the symbols of a stripped program (at its default level it would report every vulnerable symbol of an affected module as called), so the scan runs at module level: a result is a warning that says that a release was built with a module or Go version that an advisory affects, not that a binary contains or calls the vulnerable code. A finding that only the binary report has is closed by a release with the fixed version or dismissed in code scanning with a reason, not added to the allowlist of the gate. Findings that ci/vulncheck-allowlist.txt accepts are left out until the review date of the entry has passed; from the day after, they are reported again, while the gate only warns. A manual run that names a release (release_tag) reports its binary scan to its own category, govulncheck/binaries-manual, so that it does not replace the analysis of the latest stable release. The workflow blocks nothing (the gate vulncheck in compliance still does); its only write scope, security-events: write in the job upload, is listed and reviewed in ci/workflow-permissions.txt, and that job runs no code of the repository.
  • Mirror builder. scripts/build-mirror-image-crane.sh mirrors the OpAMP supervisor as well when COMPONENTS="contrib supervisor" is set (the default stays contrib): cmd/opampsupervisor/v<version>/opampsupervisor_<version>_<os>_<arch>[.exe] with its .sha256 and .sigstore.json, in the layout of the upstream releases, each bundle verified offline against the supervisor release workflow (base-binary-release.yaml) for exactly that tag. One directory (OUT_DIR) therefore serves artifacts.sourceBaseUrl of myriarchd and --release-base-url of the installers. A platform for which upstream publishes no supervisor is skipped with a warning. VERIFIER_BIN names a myriarch-updater to verify with instead of building one. ci/tests/image-crane-test.sh covers the layout, the identities and the failure cases offline.
  • Host end-to-end test. scripts/host-e2e/linux.sh manual, run by .github/workflows/host-e2e.yml on fresh Ubuntu 24.04 runner VMs (amd64 and arm64) every night, for pull requests that touch the installer, the updater, the templates of a host or the harness, and on request, installs the collector, the supervisor and the myriarch-updater with install-linux.sh as root (https, offline Sigstore verification, no --insecure) from a mirror of the real upstream release files, against a myriarchd built from the commit. It checks the services under systemd, the registration (fleet, in_sync, health), a second run of the installer that changes nothing, the refusal of a tampered archive, an update with a killed collector (automatic rollback) and its retry, and --uninstall --purge with the proof that nothing is left. The scripts refuse to run unless CI=true and HOST_E2E=1 are set, on Linux, not as root and on a host without an earlier installation; a failed run uploads an allow-list of scrubbed log files, also a run that hangs (the step that runs the scenario has a timeout of its own, below that of the job, so that it counts as failed and the upload runs, and the harness saves the files right after the diagnostics). The harness (scripts/host-e2e/lib.sh) is separate from the scenario, so that a further scenario is a function and a step of the workflow. ci/tests/host-e2e-test.sh tests the guard, the scrubber, the allow-list, the throwaway CA and the file server without a host. See “Host end-to-end test” in the development guide.
  • Kubernetes probe in CI. .github/workflows/e2e-kind.yml runs the probe of hack/kind (make kind-probe) on a kind cluster on a runner every night, for pull requests that touch the chart, the updater, the images (their build scripts and pins), the tools or the probe, and on request (PROBE_SKIP skips optional parts). The matrix entry names the pin of hack/kind/versions.env that holds the node image (image-var), so a second Kubernetes version is a pin and an entry. The job summary shows the summary block of the probe, and a failed run uploads an allow-list from .cache/kind (log, results, durations, pod logs, diagnostics) and never the kubeconfig, the generated secrets or cookie jars; the step that runs the probe has a timeout of its own, below that of the job, so that a probe that hangs still uploads. Neither workflow is part of ci-ok. ci/tests/workflow-artifacts-test.sh emulates the glob rules of actions/upload-artifact and holds the patterns of both upload steps against names that must and must not be uploaded, the timeouts of the steps that run a test below that of the job, and the path filter of the kind workflow to the files that the probe depends on, with copies of the workflows that are broken on purpose.
  • Website: findability. Every page has a unique <title> (a title from website/data/seo_titles.yaml, <tag> API for the REST API reference, or the page title) and, outside the generated references and the decision records, a description of 70 to 160 characters. sitemap.xml gives every URL a <lastmod>, the date of the last commit that changed its source file (enableGitInfo; the pages of the content adapters get theirs from website/scripts/source-dates.py), and no longer writes changefreq and priority. The website build needs the full Git history and refuses a shallow clone; the website jobs of the workflows check out with fetch-depth: 0. scripts/check-seo.py fails on a duplicate title, a description outside 70 to 160 characters and a URL without a valid lastmod. New pages: What is Myriarch?, a comparison of the OpAMP Supervisor alone, a server on opamp-go and Myriarch with sources and dates, and the guides “Manage many OpenTelemetry Collectors with OpAMP and Git” and “Upgrade the OpenTelemetry Collector on many hosts”; the FAQ answers six more questions. All are linked from the project index, the documentation index, the FAQ and llms.txt.
  • Website: canonical links, structured data and llms.txt. Every page now names its original: one absolute <link rel="canonical"> below the production origin https://myriarch.dev/, also in previews, which are built with another base URL (params.seo.canonicalBase, an origin without a path that is checked against baseURL); the 404 page has none. The home page carries structured data in JSON-LD, a SoftwareSourceCode (repository, license Apache-2.0, languages, and the version of the latest release in data/release.yaml) and a WebSite, and every page below /docs/, /project/ and /legal/ carries a BreadcrumbList that equals its visible breadcrumb (the three section pages themselves show no breadcrumb, because Docsy hides a trail of one item, and so have no list). The home page has a title that says what the project is (params.seo.homeTitle) instead of the bare site title. /llms.txt is a curated summary for language models (what the project does and for whom, key terms, the most important pages), written from data/llms.yaml by layouts/home.llms.txt through the output format LLMS of Docsy and announced in every page by a rel="describedby" link and a visually hidden line for AI agents. robots.txt states the crawler policy in a comment: every crawler may read the site. The 404 page is now noindex, because a static host may serve 404.html under its own address with status 200. The new check website/scripts/check-seo.py enforces all of it in the website gate, for the production build and for previews: one canonical link per page that names the page, JSON-LD that parses, uses only properties its type defines and equals the configuration, the release file and the visible breadcrumb, an llms.txt of the right shape (every list item below a heading is a link, as llmstxt.org has it) that links only to the site and the repository, a 404 page that cannot be indexed, no blocked crawler in the production robots.txt, a sitemap that equals the canonical links, alt text on every image and the spelling rule of ADR-0015 in the home title and llms.txt. check-indexing.py exempts the 404 page from the production noindex rule. website/README.md has the section “SEO and GEO” with the runbook for Search Console, Bing and the validators.
  • Preflight checks of the installers. Three new error codes, listed in the catalog at the top of install-linux.sh and in deploy/windows/README.md. language_mode_unsupported (Windows): the first action of install-windows.ps1 checks the PowerShell language mode, and in any mode below FullLanguage (App Control for Business with script enforcement runs an unsigned script in Constrained Language Mode) the run stops, names the file and the rule that lets it run (a hash rule created from the verified copy) and changes nothing; the installer of v0.1.0 failed on its first method call with “Cannot invoke method. Method invocation is supported only on core types in this language mode”. conflicting_install (both installers): see the upgrade notes. supervisor_missing (both installers): when the installation step of the supervisor is over, the file has to be at the path of the service, an executable file on Linux (/opt/myriarch/supervisor/opampsupervisor), a regular file with the SHA-256 that was verified on Windows (C:\Program Files\Myriarch\supervisor\opampsupervisor.exe); otherwise the run stops with the path and the next step (the protection history of the antivirus, a noexec mount). On Windows the VERSION file of the supervisor folder, which lets a re-run take the installed file as it is, is removed before the file is replaced and written only after the check, so a run that stopped at the check fetches and verifies the file again. A directory or a link at the path is not replaced: the message tells to remove it, because a new run would stop at the same item (on Windows the run stops before it downloads anything). A security tool that quarantines the unsigned binary no longer shows up only as a service that does not start.
  • Remaining health wait on the rollout page. A target that has reported success waits in verifying for the healthWait of its policy, 5 minutes in the example version policy, and the page did not say how long was left or why. It now shows the time that is left in the status column of the target list, in the step Health wait of the progress of a version target and on the card of a wave (the number of its targets in the health wait and the end of the one that finishes last). At 0 it says “finishing…” until the engine has evaluated the target. It keeps counting while the rollout is paused, because the engine keeps evaluating running targets, and it disappears when the target reaches a final status; it shows nothing for a rollout that the engine does not evaluate (failed, rolling_back and the final statuses). The step and the wave card also name the source of the wait: Policy <name> · healthWait <duration> · <path>@<commit>, with a link to the policy, where path and commit are those of the policy snapshot of the rollout. The API gets no new field: the browser computes the end as verifiedAt plus policy.healthWait (60 seconds when the snapshot has none), the rule of the engine, from what GET /rollouts/{id} and GET /rollouts/{id}/targets already return, and counts down with its own clock. The rule is in Rollouts and in Server behaviour.
  • myriarchd healthcheck. A command that checks a running server and tells the result by its exit code. It asks GET /readyz (200) and then for a WebSocket upgrade at the OpAMP path: without a token the server must refuse it with 401 (route and authentication are in place), with --token-file it must accept it with 101; the command then closes the connection in the normal way. It sends no OpAMP message, so it registers no agent and stores nothing. Flags: --url (default http://127.0.0.1:8080), --opamp-path, --token-file, --ca-file and --timeout (default 10s). Exit codes: 0 healthy, 1 not healthy, 2 wrong usage, 3 no answer. It is the health check of the container image, which has neither a shell nor curl (["CMD", "/usr/local/bin/myriarchd", "healthcheck"]), and the program of the Pod of helm test. See Server behaviour and the command reference.
  • Helm chart: helm test. The Pod of the test runs myriarchd healthcheck of the server image against the Service of the release under the restricted Pod Security profile (no user ID in the OpenShift profile, no service account token, no Secret and no environment variable). It expects the 401 for the OpAMP upgrade without a token; with tests.token.existingSecret and tests.token.key (opt-in, a Secret with an enrollment token that exists for the test only) it expects the upgrade (101). tests.resources sets the requests and limits, and tests.podAnnotations adds annotations (for example the opt-out from the sidecar injection of a service mesh; the chart sets the annotations of the hook itself, so a key that starts with helm.sh/hook is refused). The README of the chart describes the test, the NetworkPolicy rule and the opt-in.
  • Helm chart: PrometheusRule. prometheusRule.enabled (default false) renders a PrometheusRule with the 13 alerts of deploy/examples/production/monitoring/prometheusrule.yaml (no leader, more than one leader, replicas not ready, metrics unreachable, dropped and rejected OpAMP connections, unauthorized connects, release tracker, GitOps sync, rollouts, notifications and housekeeping); prometheusRule.labels adds labels, for example the one that selects the rule in the Prometheus resource. The rules select the metrics by the name of the Service (the job of the ServiceMonitor of the chart) and the namespace of the release, and the replica alert compares with replicaCount. The example for clusters without Helm has the same rules with the namespace matcher added, and ci/helm-conform.sh fails when the two differ. There is no dashboard.
  • Install plan. POST /api/v1/install/plan (operation getInstallPlan, new tag install) gives a host that is not enrolled yet its plan: the fleet it would join, the version of collector and supervisor, the OpAMP endpoint, the URLs of the artifacts on the server with an install grant for each, and whether the updater is installed (ADR-0046). The host presents the enrollment token that it will use, always, also where opamp.requireEnrollmentToken is off. The server decides the fleet with the function that assigns a connected agent (the reported site attributes and the labels of the token); a fleet named in the request that disagrees is 409. The version is the effective target of the fleet; while a version rollout of the fleet is pending, running or waiting for approval it is the version before that rollout, so that the new host becomes a straggler of the last wave; a paused, failed or rolling back rollout is 409 rollout_blocking, a fleet on hold or without a target 412 no_target_version, and a version that the host asks for is accepted inside the frame in Git only (422). A target that the leader has not planned yet (a new target in Git, a new selection in the UI) is not handed out: the answer is 503 preparing with Retry-After until the rollout planner has created the rollout, so that a new host cannot skip the waves and approvals in the time between the change and the rollout. The release tracker must know both releases. Until the collector and the supervisor for the platform are downloaded the answer is 503 preparing with Retry-After (the first request for a platform downloads about 112 MB). The plan is text/plain (one key=value line each, values from A-Z a-z 0-9 . _ : / @ + = -, at most 64 lines and 8 KiB, so that bash without jq and PowerShell 5.1 can read it) or JSON with Accept: application/json; a refusal is a problem document whose first issue carries the reason. The route is off with install.enabled: false. Nothing in the installers of v0.1.0 uses it. See Server behaviour and the API reference.
  • Install grants. A second kind of myrd_ download token for hosts that have no agent ID and no package offer yet. A grant is bound to one artifact and to the enrollment token of the request for the plan, and serves the artifact, its .sha256 and its .sigstore.json (the files an installer checks it with) until install.grantTtl has passed, and only while that enrollment token is valid: revoking the token stops its grants at once on every replica. Turning the installation off (install.enabled: false) stops them too: a server that restarts with the setting off refuses the grants that it gave before. The token of a package offer and an install grant cannot be used for each other, and the token of an offer is unchanged.
  • Install settings and metric. The section install of the configuration has install.enabled (default true), install.grantTtl (default 1 hour, or artifacts.downloadTokenTtl when that is shorter and the setting is left out; production: positive and at most artifacts.downloadTokenTtl), install.bundleSourceBaseUrl (the release source of the installer bundles of the server’s own release, default the GitHub releases of the project, never empty, production: https) and install.platforms (linux/amd64, linux/arm64, windows/amd64). The server warns at start when install.enabled is on and releases.enabled is off. The counter myriarch_install_plans_total{result} counts the plan requests.
  • Installer bundles. The server keeps the updater archive of the release that it runs for each platform of install.platforms (myriarch-updater_<version>_<os>_<arch>.tar.gz or .zip: the installer scripts, their templates and the myriarch-updater binary) and delivers it to hosts with GET /api/v1/install/bundles/{filename} (downloadInstallBundle), after the authentication of the plan: an enrollment token, always, the same 401 for every problem of a token, 429 for too many failures. The list is GET /api/v1/install/bundles (listInstallBundles, permission read). Before it fetches an archive from install.bundleSourceBaseUrl, the server verifies the keyless signature of checksums.txt of its own release against the release workflow of this project at the tag of its version (derived from the code, issuer GitHub Actions, the trusted root of artifacts.verify) and requires exactly one line for the archive that the archive matches. The archive is stored under its SHA-256, hashed again before every delivery and sent from the same open file; an archive that changed on the disk is fetched again. The bytes of the release are never templated or repacked, which keeps App Control hash rules and later Authenticode signatures valid. Only a release has bundles (MAJOR.MINOR.PATCH or -rc.N, from 0.2.0); a build from a Git checkout has none. The source is fetched like every outbound connection (destination policy, artifacts.proxyUrl, redirect rules, size limits) and without credentials, by every replica for itself. The counter myriarch_install_bundle_downloads_total{result} counts the downloads. See Server behaviour and the security model.
  • Install commands. POST /api/v1/install/commands (renderInstallCommands, permission read, the CSRF header for a cookie session) renders, for each platform, the command that a person copies to a host: it downloads the installer bundle, checks the SHA-256 that it pins before it unpacks anything, and starts the installer from the bundle. The server never sees a token: the variant token_inline has a placeholder (@@MYRIARCH_ENROLLMENT_TOKEN@@) that the web UI replaces in the browser, the variant token_file reads the token from a file on the host. On Linux the token goes through the standard input of a root shell and is read and written by builtins, reaches curl as a header through curl -K -, lives in a private mktemp -d directory of root and is deleted with it; on Windows the working directory directly below %ProgramFiles% (where standard users cannot write) is restricted to SYSTEM and the administrators before the first download, the installer starts with -ExecutionPolicy RemoteSigned for that process only (never Bypass), and the script uses cmdlets and native programs only, so it runs in Constrained Language Mode. It runs in an elevated Windows PowerShell (integrity level High) and in a session of SYSTEM (level System), the context of scheduled tasks and management agents. The text of a command is a constant; every value is a quoted argument of its own, checked against an allowlist first (no quotation mark of any kind including U+2018 to U+201E, no backtick, $, ; or control character, nothing that looks like a token), and anything else is 422. A command that carries a token needs an https server, and a server with opamp.requireClientCert renders none (412 server_settings) because the command cannot present a client certificate: such hosts use the manual path. A command starts the installer in the bundle with the options --server and --fleet (-Server, -Fleet) of its server mode, which the installers of v0.1.0 do not have: it works with the installers of the same release, which take versions and artifacts from the plan. See Server behaviour.
  • myriarchd dev --install-bundle-dir. A development server can serve the installer bundles from a directory of archives that are named like the ones of a release. The archives carry no signature, so only dev has the flag; serve refuses it with a message that says why, and the start refuses a directory outside development mode.
  • OpAMP Bridge protection in the server. The agent kind bridge (REST kind, filter kind=bridge, database migration 00007_agent_kind_bridge.sql) and the agent event bridge_locked. Without spec.bridge.manage: true on its fleet, and without standalone mode, a Bridge is offered no remote configuration whatever starts the offer; the restart command is refused for every Bridge (412 in the REST API); version rollouts skip it and say why on the agent; the server keeps no effective configuration of it. Each held-back action is an agent event and an audit entry (agent.bridge_held_back), written once per reason and not with every heartbeat. See Fleets and the OpAMP server notes.
  • Installation from the server on Linux. install-linux.sh --server <url> --token-file <file> takes the versions, the OpAMP endpoint and the artifacts from the server instead of from the operator (ADR-0046). It asks POST /api/v1/install/plan (the token only as a request header, read by curl from standard input) and reads the plan strictly as data: known keys only, each value against a pattern, at most 64 lines and 8 KiB, URLs of the origin of --server only, https only, redirects only to the same origin. Collector and supervisor are downloaded with the install grants of the plan and verified with SHA-256 and their Sigstore bundle against an identity that the installer builds itself; the verifier is the myriarch-updater of the installer bundle (verify-bundle), so the host needs no cosign, and nothing runs before its check. A plan that the server is still preparing is awaited (--plan-wait, default 15 minutes, after Retry-After). New error codes plan_failed, plan_invalid, plan_rejected (with the code of the server) and plan_unavailable, each with the next step; https_required, conflicting_install and supervisor_missing apply as before. The commands that the server renders for Linux run unchanged with the script (--server, --token-file, --ca-file, --fleet, --insecure and the site options). See Connect a host and Installing a host from the server.
  • Host end-to-end test: scenario one-command. scripts/host-e2e/linux.sh one-command creates a token at run time, renders the command through POST /api/v1/install/commands, runs it unchanged as root on a fresh runner and checks a healthy agent in the expected fleet at its target version, a configuration rollout, a version update through the updater with a rollback and the retry, and the uninstallation with the proof that nothing is left. The workflow host-e2e.yml runs it after manual (the bundle is built from the commit and served from a directory by the development server), the path filter names internal/install/** and the parts of the bundle, and a manual run takes the input release_tag (for example v0.2.0) to install from the published, keyless verified installer bundle of that release instead. The token is masked in the log, and the upload takes an allow-list of files, never the token file or the rendered command.
  • Installation from the server on Windows. install-windows.ps1 -Server <url> -TokenFile <file> does what the Linux installer does (ADR-0046): it asks POST /api/v1/install/plan (the token only as a request header), reads the plan strictly as data (known keys only, each value against a pattern, at most 64 lines and 8 KiB, URLs of the origin of -Server only, https only, redirects only to https on the same origin, nothing evaluated as code), downloads collector and supervisor with the install grants of the plan and verifies them by SHA-256 and their Sigstore bundle against an identity, issuer and version that it builds itself, with the myriarch-updater.exe of the installer bundle as verifier, so the host needs no cosign.exe. New parameters are -Server, -Fleet and -PlanWait; the error codes plan_failed, plan_invalid, plan_rejected and plan_unavailable apply as on Linux, next to https_required, conflicting_install, supervisor_missing and language_mode_unsupported. The Windows commands that the server renders run unchanged with the script (-Server, -TokenFile, -Fleet, -CollectorVersion, -Insecure and the site options). The command sets the execution policy for its PowerShell process only (never Bypass, no change of the machine or user policy) and reports a policy from Group Policy instead of working around it. The working directory of the installer is made below %ProgramFiles% with a DACL for SYSTEM and the administrators before the first download. The certificate of the server is checked against the certificate store of the machine. The installer bundle has to be in a directory that only SYSTEM and the administrators can write (the command unpacks it below %ProgramFiles%), checked before the first request (directory_unsafe), because its myriarch-updater.exe verifies what is installed and runs as a SYSTEM service. The Windows files are not Authenticode-signed: AllSigned blocks them and App Control needs a hash rule for each release. See Connect a host and Installing a host from the server.
  • Host end-to-end test on Windows. The job windows of host-e2e.yml runs scripts/host-e2e/windows.ps1 one-command on a fresh windows-2025 runner, in Windows PowerShell 5.1 and in PowerShell 7 at the same time. It builds myriarchd (embedded PostgreSQL), myriarch-updater and myrctl from the commit, serves the installer bundle (zip) from a directory by the development server, creates an enrollment token at run time, renders the command through POST /api/v1/install/commands and runs it unchanged in an administrator session. It checks a healthy agent in the fleet windows-hosts at the target version of that fleet, the two services (the supervisor under NT SERVICE\otelcol-supervisor, the updater as LocalSystem), the ACL of the registry key that holds the token and of the data directories, a configuration rollout, a version update through the updater with a killed collector (rollback by the updater) and the retry, and install-windows.ps1 -Uninstall -Purge with the proof that no service, file, registry key, process, listener or working directory is left. The script refuses to run unless CI=true and HOST_E2E=1 are set, on Windows, elevated, on a GitHub-hosted runner and on a host without an earlier installation; the throwaway CA is in the certificate store of the machine only while the scenario runs; the token is masked in the log and the upload takes an allow-list of scrubbed files, never the token file or the rendered command. The path filter names the Windows installer, deploy/updater/windows/**, deploy/supervisor/windows/** and deploy/windows/**, and the input release_tag also runs the scenario against the published bundle of a release. The job runs in CI only; the support level of Windows stays “experimental” until it is green. ci/tests/workflow-artifacts-check.py checks the shape of both jobs and ci/tests/host-e2e-test.sh the helpers and the guard under pwsh.
  • Dialog “Add host” in the web UI. Agents and the enrollment tokens (Settings, Tokens) have a button Add host. The dialog renders one ready install command for each platform (Linux amd64, Linux arm64, Windows amd64) from POST /api/v1/install/commands, with a copy button, an optional fleet hint and the site attributes, and the variants Token in the command (the default) and Token from a file. Everyone with read can open it; creating the token needs enrollment:manage, and a user without it pastes an existing token or uses the file. The token lives in the state of the dialog only: no browser storage, URL or log, never in the request to the server, masked on the page, and dropped when the dialog closes; the dialog warns that clipboard, shell history, PSReadLine, PowerShell transcription and script block logging, and terminal recordings keep it. It explains a server without bundle (a local build), a bundle that is being prepared or failed, a server that requires client certificates and refused input. The install example in the dialog that shows a new enrollment token is gone; the dialog offers Add host with this token instead. Connect a host has the section “The quick way: the dialog Add host”.
  • Installation test of both charts on kind with two Kubernetes versions. ci/kind-chart-test.sh installs the server chart (with helm test), the collector chart as a StatefulSet under the Pod Security level restricted and as a DaemonSet in a privileged namespace on a fresh kind cluster, checks that every agent registers healthy at the server through the REST API, uninstalls cleanly and runs a helm upgrade from the chart of v0.1.0 to the chart of the commit, with PASS / FAIL per step. The new job charts of .github/workflows/e2e-kind.yml runs it on two Kubernetes versions (the new pin KIND_NODE_IMAGE_PREVIOUS in hack/kind/versions.env next to KIND_NODE_IMAGE) and uploads the same allow-list of logs and diagnostics after a failure. Chart installation test.
  • Kubernetes Quickstart on kind. hack/kind/quickstart.sh (make kind-quickstart, kind-quickstart-smoke, kind-quickstart-rollouts, kind-quickstart-all) creates a kind cluster, installs the server chart with a demo PostgreSQL, a demo Git server and a bootstrap admin, installs the collector chart as a gateway (a StatefulSet with two replicas in container mode, on collector 0.161.0) and as a node agent (a DaemonSet), optionally starts 20 simulated collectors, checks the installation without signing in, and runs a first configuration rollout and a first version rollout through the REST API with the waves approved. The page Kubernetes on kind (available from v0.2.0, with the published OCI charts) uses the same script; QUICKSTART_SOURCE=commit installs the charts and images of a checkout, which the new job quickstart of .github/workflows/e2e-kind.yml does, measuring the duration of every step into the summary of the run. KIND_SYSTEM_TOOLS=1 makes hack/kind/up.sh use kind, kubectl and helm from the PATH. make validate-examples also validates the Git repository of the Quickstart.
  • The page Support levels and tested platforms lists the support level of every component and the platforms of the automated tests: Ubuntu 24.04 (amd64 and arm64) and Windows Server 2025 (amd64, Windows PowerShell 5.1 and PowerShell 7) for hosts, and two Kubernetes versions on kind for the charts. The release notes carry the same list.

Changed

  • Kubernetes collectors: the Kustomize base and the combined image are deprecated. deploy/k8s/collector (with the Dockerfile of the combined collector image), deploy/supervisor/k8s, deploy/updater/container/updater.yaml and scripts/build-collector-image-crane.sh moved to contrib/legacy-kubernetes-collector with a README. They receive no support and will be removed once the kind probe and the production example use the chart. Use the collector chart instead (how to move); it needs no root init container, builds no user ID into an image (10001 is an overridable default of the standard profile; the OpenShift profile sets none) and needs no registry of your own.
  • Website. The search index loads when a visitor first uses a search box, not on every page load. Docsy’s script fetched the index (about 1.2 MB of JSON, 375 KB compressed) and built the lunr index at DOMContentLoaded; that one long task, about 0.5 s on a phone, was the whole Total Blocking Time of the landing page (440 to 490 ms, performance 72 on mobile and 80 on desktop in PageSpeed Insights). A page now requests the index when a search box first receives the focus, a pointer press or an input, once for all boxes on the page, and renders a query that is submitted before the index has arrived as soon as it has; the results are the same. A load that fails (the network is down for a moment, or the tab was opened before a deployment and names an index file that the host no longer serves) is logged and tried again with the next query, and a query that waited for it gets a hint in its popover that asks for a reload. In a Lighthouse lab run with the throttling applied to the browser, the Total Blocking Time of the landing page on the mobile profile falls from about 400 ms to 0 ms and the performance score rises from 89 to 98. The script is an override of assets/js/offline-search.js of Docsy 0.18.0, pinned in website/overrides.txt; check-overrides.py now also covers the files of website/assets that replace a theme file, and check-requests.mjs fails when a page requests the index while it loads, when the search requests it other than once or when it does not recover from an index that fails to load.
  • Start and migrations. myriarchd serve, myriarchd dev and myriarchd migrate share one schema step. An incompatible schema is refused before anything is changed, a newer schema within the compatibility level starts with a warning, and the log line database schema ready names the schema version, the compatibility level and the schema level. The generated references (configuration, environment variables, CLI) state that migrate loads the database settings only.
  • Tool installation. ci/install-tools.sh retries a download after a transient failure instead of failing the job: a release download (kubectl, shellcheck, pwsh, Hugo, Dart Sass, lychee, syft, zizmor, the source of osslsigncode) after a timeout, a failed connection or HTTP 408, 429, 500, 502, 503 or 504, with pauses of 1, 2, 4, … s and no attempt later than 120 s after the first (the policy of the installers), and a Go tool (kind, helm and the others) up to four times, 5, 10 and 20 s apart, when go build fails with a network error of the module download. A 404, a certificate error, a checksum that differs from the pin, a failed module verification and a compiler error still end the script at once, and the checks after a download are unchanged. smoke-kind of the release workflow benefits; docs/releasing.md and ci/release/smoke.sh say which steps still have a shorter policy of their own.
  • Windows installer: ARM64 stops before anything is changed. Without -Arch amd64 the installer stops with unsupported_platform as soon as it knows the architecture, before the work directory, the downloads, the verifier and the stop of the service. The installer of v0.1.0 stopped only at the supervisor step, after it had stopped the service and installed the ARM64 collector, which the next run kept.
  • Windows installer: a service that does not start is explained by its file. When otelcol-supervisor is not running at the end of the run, the installer first checks the file of the binPath that the service has in the registry. A file that is gone is reported as supervisor_missing, with the path, in place of service_not_running; otherwise service_not_running is as before. At the top of the script, the error line is printed with Write-Host in a restricted language mode, where a method call on the host is refused.
  • The rollout planner records the target it handled. After it planned a fleet without an error, the planner writes the effective target version of the fleet to the key-value store (rollout/planned-target/<fleet>, when it changes and after it created the rollout; empty for a fleet without a target), next to plannedAt of a selection. The installation of hosts from the server reads it on every replica and waits until it equals the target (Planner). There is no setting and no migration; a leader writes the records again when it starts a term.
  • Demo values in the Quickstart policies. examples/quickstart/policies/config-canary.yaml and canary-then-all.yaml, the Git source of the Docker Compose Quickstart and the demo policies of the kind Quickstart, now have healthWait: 30s (was 2 minutes for a configuration and 5 minutes for a version), maxConcurrent: 0 (unlimited, was 5) and no maintenance windows in canary-then-all (it had two). A rollout in a Quickstart is completed about a minute after the approval of the last wave and no longer waits for the evening. The production-like values stay in gitops-template/config/policies/ with a comment on each value: healthWait 2m for a configuration and 5m for a version, maxConcurrent: 5, and the commented maintenance window. A repository that you copied from examples/quickstart keeps its own values; the image of the next release carries the new demo policies, so a Compose Quickstart that you start from it rolls out faster. The pages of the Quickstarts and Rollouts name the health wait.
  • Support levels of v0.2.0: the server Helm chart is beta (it was experimental), the collector Helm chart is experimental, Linux hosts and container mode stay beta, Windows hosts stay experimental.
  • ROADMAP.md shows what v0.2.0 contains. The production Docker Compose setup with TLS and a Git source is no longer planned for v0.2.0; it is listed under later. The operations guides (upgrades in general, backup and restore, monitoring, air-gapped installation, artifact mirror) are planned for v0.3.0, and their placeholder pages on the website say so.

Fixed

  • Artifacts with the same content on a Windows server. Two artifacts with the same content (for example the same file under two platforms) are stored at one content-addressed path, and their downloads end in a rename onto it. Windows can refuse to replace a file that is being replaced or read at that moment, so one download was recorded as failed although the file was already in place, and the install plan kept answering 503 preparing. A rename that fails is now accepted when the target is a regular file whose SHA-256 (hashed again) equals that of the download; every other case keeps the error. The same applies when a replica fetches an artifact again.
  • Install examples in the enrollment-token dialog. The Linux and Windows examples left out inputs that both installers require: without --collector-version (-CollectorVersion) they stop with option_missing, and without a Sigstore verifier with sigstore_verifier_missing. They also started the script next to nothing, although it reads its templates from the directory above its own and has to run from the unpacked updater archive, and the Windows example started install-windows.ps1 directly, which the default execution policy Restricted refuses. The examples now run deploy/scripts/install-linux.sh and deploy\scripts\install-windows.ps1 from that archive, name the collector version and the verifier (--verifier, -Verifier), and start the Windows installer with powershell.exe -NoProfile -ExecutionPolicy RemoteSigned -File ..., which sets the execution policy for that one process and changes no setting of the machine. The Linux example runs in a root shell, in a directory that only root can write, without sudo, and takes the verifier as a full path (cosign, or the myriarch-updater of the archive), because a directory or a cosign that the invoking user can write lets that user change what root runs. The token still reaches the installer only through a file that a shell builtin or Set-Content writes and the example deletes, and it appears in no other line. The Windows example gives that file its own DACL for SYSTEM and the administrators before the token is written, because a folder below C:\Program Files that only administrators can write can still be read by every local user. Both examples say in a comment that the pasted command, token included, is recorded by a shell history, a PowerShell transcript or script block log and a terminal recording. internal/updater/deploy_snippets_test.go reads the required options from the installers and fails when the examples do not name them.
  • Configuration targets no longer wait for the applyTimeout after a restart in the middle of applying. An agent that restarted while it applied a configuration reported the stored APPLYING for the desired hash after it reconnected, and the server never offered a hash again that the agent reported as APPLYING; the rollout target stayed in progress until its applyTimeout. The server now offers the configuration once more after opamp.staleApplyingAfter (see the upgrade notes). The documentation of the supervisor behaviour is in Upstream notes.
  • Updates of Linux hosts stopped in the preflight. The systemd unit myriarch-updater.service set NoNewPrivileges=true, and with it the updater could not start the new collector under the account of the supervisor for the preflight (otelcol-contrib validate): every update through the updater failed in the step verifying with “operation not permitted”, and the old version kept running. The unit no longer sets it. Hosts that were installed with v0.1.0 get the new unit when the installer of this release runs again; until then a drop-in with NoNewPrivileges=false helps (Troubleshooting). The host end-to-end test found the defect.
  • Usage examples in the headers of the installation scripts. The example in install-linux.sh put the token on the command line and started the script with sudo from the current directory, the examples of install-windows.ps1 and Set-ServiceEnvironment.ps1 named a file below C:\Temp, which unprivileged accounts can write, and started the script directly, which the default execution policy refuses. They now show what Connect a host documents: a root shell and the script from the verified archive, the token in a file that is deleted afterwards, a verifier, and on Windows Unblock-File after the checks and powershell.exe -NoProfile -ExecutionPolicy RemoteSigned -File ... with a file in the setup folder that only SYSTEM and the administrators can read. Tests keep the examples in step with the scripts.
  • A fleet change is recorded once with several replicas. Two replicas that evaluated the same agent at the same time (AssignAgent on one, ReassignAll on the other) could both write the assignment, record the fleet change twice and reset the desired state twice, which also cleared failed_config_hash. The assignment is now a compare-and-set in the database. AssignAgent reads the snapshot after it has the agent’s lock, a listing older than five minutes is treated as stale, and pruning of the record of recent writes is throttled instead of a full scan under the lock on every write.
  • Windows installer with -WithUpdater. The check of the data directory after the services have started no longer stops with directory_unsafe when the supervisor has created files of its own: the supervisor’s service account is accepted as owner only below its two data directories (supervisor state and collector storage); everywhere else, any owner other than SYSTEM, the administrators or TrustedInstaller still stops the installation.
  • The collector chart is Ready with its default values, also while no fleet is assigned to the agent. Before, the readiness probe on port 13133 got “connection refused” once the supervisor was connected and the server sent no configuration, because the no-op pipeline that the supervisor runs then has no health_check; the fallback configuration applies only while the server is unreachable at startup.
  • The main container of the collector chart is no longer OOM-killed with the default values. The default memory limit was too low for the supervisor and the contrib collector together.
  • The seed init container of the collector chart is no longer OOM-killed while it copies the collector binary.

Security

  • Windows files of v0.2.0 are unsigned. The SignPath signing is not set up yet, so v0.2.0 ships myriarch-updater.exe, myrctl.exe and the two PowerShell scripts without Authenticode signatures, under a reviewed exception for exactly that tag (WINDOWS_UNSIGNED_FINAL_TAG in deploy/release/windows-signing.env, ADR-0034). Verify them with cosign before you run them (the security model).
  • Go 1.27.3. The Go toolchain of the main module and the Go build images (myriarchd, myriarch-sim, updater build of the collector image) move from 1.27.2 to 1.27.3, which fixes GO-2026-6809 in crypto/x509 reported by govulncheck.
  • Collector chart: Pod Security level restricted, OpenShift without a user ID. The standard profile sets runAsUser, runAsGroup and fsGroup to 10001 as overridable defaults (null leaves a field out), because the supervisor image runs under a named user and the kubelet cannot check runAsNonRoot without a numeric ID. The OpenShift profile renders none of the three. The pods add no capability, use a read-only root file system and seccompProfile: RuntimeDefault, and mount no API token. The chart creates no Role. The Kustomize base ran an init container as root to change the owner of the node directory; the chart has none. The one exception is a read-only hostPath of the node logs and the optional hostPath for the identity in the DaemonSet, which the Pod Security levels baseline and restricted refuse; both are off by default and documented.
  • Helm chart: secrets and the development mode are not values. The values schema offers no key for a database DSN, an OIDC client secret, a webhook secret, the URL of a notification channel or an enrollment token, refuses a Secret reference that has the form of an enrollment token, and offers neither config.dev nor config.auth.devLogin; the templates keep their own refusals for a run with --skip-schema-validation. A test fails for a new setting of the configuration that can hold text (a string, a list of strings or a map of strings) and names a token, secret, password, key, credential, passphrase, bearer token or DSN, unless it holds a file, a directory or a prefix. The controls and their residual risk (free-text values such as annotations or extraEnv[].value are not recognized as secrets) are in the section “Helm chart values” of the security model.
  • Helm chart: the switches of the TLS checks are booleans. ingress.allowWithoutTls and httpRoute.tlsTerminatedAtGateway must be booleans, and openshift.route.tls.insecureEdgeTerminationPolicy one of Redirect, Allow and None. A template counts the string "false" as true, so with the templates alone ingress.allowWithoutTls: "false" switched the TLS check of the Ingress off; the schema refuses it. The external entry points keep using TLS unless you opt out: the server refuses a server.publicUrl that is not https in production, an Ingress needs ingress.tls and an HTTPRoute httpRoute.tlsTerminatedAtGateway for it, the install notes print wss://, and the OpenShift Route redirects plain HTTP unless the policy is Allow, the explicit opt-in for plain HTTP and ws:// at the router. Port 8080 inside the cluster is plain HTTP, limited by the NetworkPolicy; see the residual risk in the security model.
  • Unpinned package manager install removed. ci/test-web.sh had no caller and was the only place that installed pnpm with npm install --global without a hash, the open Pinned-Dependencies finding of the OpenSSF Scorecard. The script and the comment that mentioned it are gone. The web gate and the CI jobs take pnpm from corepack, at the version of the packageManager field of web/package.json.
  • myriarch-updater.service without NoNewPrivileges. The preflight needs to switch to the account of the supervisor. The rest of the sandbox stays (capability bounding set, ProtectSystem=strict, RestrictSUIDSGID, restricted namespaces and kernel interfaces); the residual risk is in the security model.
  • Installers: preflight checks that fail closed. A host that is not fit for the installation is refused before anything is downloaded, stopped or written, and nothing is removed: a language mode below FullLanguage on Windows (the check and its error line work in Constrained Language Mode), the services and units of the packages of the upstream project, and Windows on ARM64 without -Arch amd64. After the supervisor step the installers check that the supervisor is a regular file at the path of the service, and on Windows the marker that lets a re-run skip that step is written after the check, so a changed file is not accepted on the next run. The controls and their limits (the check knows two service names and two unit names; the check of the file sees one moment) are in “Preflight checks of the installers” in the security model.
  • Health check. myriarchd healthcheck reads the enrollment token from a file only, never from an argument, the environment or standard input, and never prints it; the file must be a regular file of at most 4 KiB with one enrollment token, so a file with another secret (the database DSN, the token of the bootstrap admin) is refused before a request is made. The token goes to the host of --url only: no redirect is followed, no proxy is used and the certificate of an https server is always verified (--ca-file narrows the trust to the CAs of that file; a wrong CA fails the check). The command sends no OpAMP message. A check without a token counts as an unauthorized OpAMP connect in myriarch_opamp_connect_rejected_total and myriarch_auth_agent_requests_total. See Server behaviour.
  • Helm chart: the test Pod. The Pod of helm test follows the restricted Pod Security profile, mounts no Secret and has no environment variable without the opt-in, and its container settings are fixed; the chart refuses the Secrets of the server itself as the test token, and the command refuses a file that holds anything but an enrollment token. Its NetworkPolicy source (with allowSameNamespace: false) can be used by every pod that carries the labels of the test Pod, so it never exists together with trustedProxies or clientCertHeader. The controls and their residual risk are in the section “Health check and Helm test” of the security model.
  • The routes of the installation always need an enrollment token, and their failures are limited. The plan (and with it the grants) is authenticated by the enrollment token also where opamp.requireEnrollmentToken is off, with the lookups and the client certificate rule of the OpAMP endpoint. A missing, malformed, unknown, revoked or expired token gets the same 401, so a request cannot tell whether a token exists. The routes can be reached by anyone and share the bound on token lookups with the OpAMP endpoint, so failed authentications are also limited per client address (30 free, then one per second, IPv6 per /64): an address without budget is answered with 429 before any lookup, and one address cannot use up the lookups that enrolling agents need. The token is read from one header and never written to a log, a metric label, an audit entry, a URL or an answer. See Host installation from the server.
  • The command that installs a host keeps the token out of arguments, URLs and logs. See the bullet “Install commands” above: the token is read and written by builtins, sent as a header only, kept in a private directory and deleted with it, and the command checks the pinned SHA-256 before it unpacks or runs anything. What typed text leaves behind (shell histories, PSReadLine, PowerShell transcription and script block logging, session recordings) is outside these controls; hosts that record their input use the variant with the token from a file. The first installation trusts the server that displays the command (trust on first use); the security model lists the residual risks, and the manual path stays for sites that cannot accept them.
  • A new host cannot skip the waves of a version change. The plan waits (503 preparing) until the rollout planner has created the rollout for a new target, so a host that is installed in the time between a change in Git or in the UI and the rollout joins the last wave of that rollout, instead of taking the new version past the waves and approvals. Install grants also stop working when install.enabled is turned off, and the commands are not rendered for a server that requires a client certificate. See Host installation from the server.
  • Windows installer: data directory reset. Before it changes any permission or owner in C:\ProgramData\Myriarch, the installer stops with directory_unsafe if a file there has more than one hard link, so that the change only reaches files of the data directory. The supervisor’s service account is accepted as owner only below its two data directories, not for the directories themselves.

Documentation

  • Collectors on Kubernetes. The new page Install the collector with Helm (installation, modes, Pod Security, OpenShift, container mode, moving from the Kustomize base) and Container mode describe the chart and its unchanged upstream images. The release list in Releasing covers both charts.
  • Helm chart. Install with Helm has the sections “Registry and repository of the image” and “The values schema” with the findings to expect when you upgrade from v0.1.0. The README of the chart describes the image values, the schema and the two layers of misconfiguration messages (the schema and the templates). The development guide lists the values schema among the generated files and the rules for changing it, AGENTS.md and CONTRIBUTING.md name it in the list of generated files, and the security model describes the controls.
  • Upgrade and rollback. The server behaviour (“Start, migrations and schema compatibility”), the security model, the troubleshooting page, the architecture page, the Helm installation page and the chart README describe the schema check, the error messages and the rollback rules. The development guide explains how to write a migration, how to choose the compatibility level and how the changelog sections (“Upgrade notes”, “Added”, “Changed”, “Fixed”, “Security”, “Documentation”) are used.
  • Host installation from the server (decision record). ADR-0046 records how hosts are installed from the server. myriarchd provides the plan (fleet, versions, OpAMP endpoint), the artifacts and the installer bundle of its own release as data, and the host verifies every file with SHA-256 and Sigstore against an identity that the installer builds itself: identity, issuer and trust root never come from the server. The first installation trusts the hash pinned in the copied command (trust on first use), and the manual verified path stays for sites that cannot accept that. The record also fixes the rules for the token in a copied command, the version that a new host installs, the Windows start with an execution policy for one process only (never Bypass, never a machine-wide change) and conflicting_install for packages of the upstream project. It also states what the design does not prevent: a compromised server can steer a new host to an older genuine upstream version, and a pasted command that carries the token is recorded by PowerShell transcription and script block logging, the history of a shell and session recordings, so hosts that record input use the variant with the token from a file. It refines ADR-0007, ADR-0012 and ADR-0013. The installation with one command ships in this release, and the manual path of docs/tasks/connect-a-host.md stays supported.
  • Connect a host. docs/tasks/connect-a-host.md lists every input of the installers of v0.1.0 (the OpAMP endpoint, the token, the site type and ID, the required collector version, the installer release and a Sigstore verifier) and every step. It also says which collector version a new host installs when its fleet has no target version, is on hold or runs a version rollout. Download the updater archive of a release, verify it as Verify releases describes, unpack it and run the script from the unpacked archive, because it needs its templates. The Linux steps run in a root shell in a directory below /root, and the Windows steps in a folder with a DACL of its own for SYSTEM and the administrators, which also keeps the token file from other local users; the checks are chained, so that the first failure unpacks nothing. Choose a verifier: cosign 3.0.4 or newer, on Windows from a folder that only administrators can write, the myriarch-updater of the verified archive, or the updater verified by its SHA-256. The host needs https access to the Sigstore TUF repository (tuf-repo-cdn.sigstore.dev) or a local trusted_root.json. On Windows the guide shows the start with powershell.exe -NoProfile -ExecutionPolicy RemoteSigned -File ... after the checks and Unblock-File, names what to avoid (Bypass, a change of the policy of the machine) and what wins over it (Group Policy). The new table “When the installer stops” covers option_missing, usage_error, unsupported_platform (an unsupported CPU, and Windows on ARM64 without -Arch amd64), file_unreadable, sigstore_verifier_missing, sigstore_verification_failed (including a host that cannot reach the Sigstore TUF repository), template_missing, a script that PowerShell refuses, and a supervisor or collector installed from an MSI of the upstream project, which is another service under another name and path that the installer of v0.1.0 neither detects nor removes. deploy/windows/README.md, docs/security/verify-releases.md, deploy/README.md, the troubleshooting page and the known limitations of docs/security/model.md describe the same start.
  • AGENTS.md lists the tests that use fixed ports, a shared cluster or the machine itself, and says that scripts/host-e2e runs in CI only. The development guide has the sections “Host end-to-end test” and, for the kind probe, “In CI”; the security model describes the controls of both workflows. docs/releasing.md no longer says that no CI job runs kind.
  • Preflight checks of the installers. Connect a host marks the behaviour of v0.2.0 and has the rows for conflicting_install, supervisor_missing and language_mode_unsupported in “When the installer stops” (cause and next step on both systems), and the rows for unsupported_platform and service_not_running say what changed. The README of the Windows installer has the new section “Language mode and App Control” and the codes in its table, Verify releases says what the installers check before they change anything and how to allow the script under App Control, and the security model describes the controls and their residual risks. The deploy README and the troubleshooting page point to the codes.
  • Health wait. Rollouts describes the rule (a target in verifying succeeds when the agent is online and healthy and now - verifiedAt >= healthWait), the default of 60 seconds, spec.healthWait, what fails a target during the wait (an agent that goes offline or reports an error), that the wait goes on while a rollout is paused, and where the page shows the remaining time. The comments of examples/quickstart/policies/canary-then-all.yaml and config-canary.yaml say why the example policies keep 5 minutes and 2 minutes.
  • Health check. The server behaviour specifies the command (flags, requests, output, exit codes and the metrics of a check), the development guide names its exit codes, and the architecture page mentions it. The command reference is generated.
  • Helm test and alerting rules. Install with Helm and the README of the chart describe helm test, the opt-in token, the NetworkPolicy rule and the PrometheusRule, the troubleshooting page lists the failures of the test, and the security model describes the controls.
  • Installation from the server. Server behaviour describes the plan, its version rule, its line format and its errors, and the artifact cache section the install grants and the preparation without waiting. Version selection names the plan as a consumer of the effective target. The security model has a new section with the controls and the residual risks, and the sections on token formats, agent authentication, artifact verification and the known limitations mention the grants and the routes. The production checklist lists the install settings. The configuration, environment variable, metrics and API references are generated.
  • Installer bundles and install commands. Server behaviour describes where a bundle comes from, what is verified first, where it is kept, when it is prepared and how it is delivered, and the commands with their inputs, the steps of the Linux and the Windows command and what they cannot hide. The security model has the new controls, the residual risks (trust on first use, the records of typed text, Group Policy and App Control) and a row of the outbound connections for the release source; the production checklist names install.bundleSourceBaseUrl, and the development guide the fixtures and the tests that run the commands.

Known issues

  • A nightly browser test of updates from the UI fails at one check. The job ui-updates of the e2e workflow does not always see the short health-check phase of an update before the update ends. The other checks of the test pass. The job is not part of ci-ok or of the tag guard.

[0.1.0] - 2026-10-13

First final release. The release candidates v0.1.0-rc.3 and v0.1.0-rc.4 were published before it as GitHub pre-releases. The sections below describe what the project ships; the history before publication is in the Git log.

Added

Defaults. Without configuration, myriarchd behaves neutrally:

AreaDefault
Maintenance window time zonerollouts.defaultTimezone is UTC
Artifact prefetchartifacts.prefetchPlatforms is empty
UI languageEnglish, with detection from the browser language (ui.defaultLanguage, ui.languages)
Login path in serve modemyriarchd serve requires auth.oidc.enabled or auth.bootstrapAdmin.enabled
Webhook and AWX targetsexecutors.allowedWebhookHosts is empty and refuses every target (fail closed, in every mode)
Helm chart ingressingress.enabled is false
Helm chart GitOpsconfig.gitops.enabled is false
Helm chart network policyNo fixed ingress-controller or monitoring namespace; port 8080 is open to the pods of the release namespace (networkPolicy.ingress.from and networkPolicy.metrics.from widen it)
Helm chart storageemptyDir.sizeLimit 4Gi, persistence.size 10Gi, config.artifacts.maxCacheBytes 3 GiB
Helm chart download tokensconfig.auth.downloadTokenSecretFile is empty (key stored in the database, shared by all replicas)
  • Time zone. Rollout policies whose maintenance windows follow the agent’s time zone (timezone empty or agent) and that set no spec.defaultTimezone fall back to rollouts.defaultTimezone for agents without a valid time zone attribute. myriarchd logs one warning at startup that lists such policies and names the effective default, and the rollout engine logs the same warning once per policy the first time it plans a rollout with it.
  • Prefetch. The prefetch of new target versions adds the platforms of the members of updater fleets. An explicit prefetch request (POST /artifacts/actions/prefetch) that names no platforms uses artifacts.prefetchPlatforms; if that is empty too, it uses the distinct platforms of the connected collectors of the fleets that use the requested distribution. The request fails with 422 when that set is empty as well.
  • UI language. The UI picks the language in this order: the choice stored in the browser, the first supported browser language (de-AT matches de), the server default ui.defaultLanguage, English. The first day of the week in the date pickers follows the language.
  • Fail-closed allowlist. An empty executors.allowedWebhookHosts refuses every webhook and AWX target without sending a request.
  • Login path. myriarchd serve, myriarchd config validate and myriarchd migrate fail with a clear error when neither single sign-on nor the bootstrap admin is configured. The development login exists only in myriarchd dev (ADR-0025). The Helm chart enables the bootstrap admin by default (bootstrapAdmin.enabled: true) and generates a one-time token.
  • Feature-gated secret files. myriarchd reads a *File setting only while its feature is active: the OIDC secret and certificate files while auth.oidc.enabled, the bootstrap files while the bootstrap admin is active, the GitOps credentials and gitops.webhookSecretFile while gitops.enabled, and releases.tokenFile while releases.enabled. The chart renders these file paths only for active features.
  • Interfaces. The binaries are myriarchd (server), myrctl (CLI), myriarch-updater (host updater) and myriarch-sim (simulator); the short CLI alias myr is documented in docs/reference/cli/myrctl.md and not shipped. The GitOps apiVersion is myriarch.dev/v1alpha1. The images are ghcr.io/myriarch/myriarch and ghcr.io/myriarch/myriarch-updater, and the Helm chart is deploy/helm/myriarch (ADR-0015, ADR-0029).
  • Web UI. The UI shows the project logo (the tetractys mark) in the favicon, the header, the login page and the loading screen, and serves favicon.svg, favicon.ico and apple-touch-icon.png. The logo files and the brand guide are in assets/brand/.
  • Rollout page. The rollout targets list shows targets with running work first (in progress and verifying, then waiting for a maintenance window, failed and rolled back, pending, succeeded, skipped, cancelled) and can switch to the wave order. A wave card shows “Approval required” only before the approval, and “Approved” after it. The wave percentage is labelled as the cumulative share of all targets. Progress bars draw pending targets as empty track and use yellow only for targets waiting for a maintenance window.
  • API. GET /api/v1/rollouts/{id}/targets accepts sort=wave (default, unchanged order: wave, hostname, agent ID) or sort=activity, and agent=<agent ID> to return the target of one agent. The open detail view of a target on the rollout page follows its target when the row moves to another page.
  • Examples and tooling. myriarchd dev reads examples/quickstart by default (--gitops-dir); make dev, make validate-examples and ci/verify-gitops.sh use it. make sim, myriarch-sim and the compose and image examples use examples/sim/mixed-fleet.yaml. The e2e load run is E2E_AGENTS=1000 scripts/e2e.sh. The crane image scripts require IMAGE_REPO, read the OCI labels from ci/image-labels.env and derive the source label from SOURCE_URL or the git remote. The mirror image uses the public busybox base, pinned by digest.
  • Installers. Every error and warning line of the installers carries a stable code: [myriarch-install] ERROR [<code>] ...; the catalog is in deploy/scripts/install-linux.sh and deploy/windows/README.md. Helm fail messages start with the value path, for example config.server.trustedProxies: ....
  • API and web UI version. The version of the REST API (info.version) and of the web UI package follows the release version instead of 1.0.0-alpha.1 (Releasing).
  • Download retries. The installers (install-linux.sh, install-windows.ps1) and the release smoke tests (ci/release/smoke.sh, ci/release/smoke-windows.ps1) retry a download after a transient failure: a timeout, a failed connection, or HTTP 408, 429, 500, 502, 503 or 504. The pauses double from 1 second (1, 2, 4, … s), and no attempt starts later than 2 minutes after the first one, so a short outage of github.com or of a mirror no longer stops an installation or a smoke test. Behind an HTTPS_PROXY that refuses the connection, the status of the proxy decides (503 is retried, 407 is not); the PowerShell scripts need PowerShell 7.5 or later for this, and PowerShell 7.4 retries such a refusal like a failed connection. Any other failure, for example a 404 or a certificate error, still fails at once, and [download_failed] names the number of attempts. Before, install-linux.sh gave up after 4 attempts within about 6 seconds and did not retry a refused connection, and install-windows.ps1 tried every download three times, a 404 included. The https, SHA-256 and Sigstore checks are unchanged.
  • Installer prerequisites. install-linux.sh checks for gzip next to tar and stops with [tool_missing] that names the tool, instead of reporting a valid collector archive as [archive_invalid]: GNU tar runs gzip to unpack the .tar.gz archives.
  • Keyless updater verification. The installers verify the updater through the keyless Sigstore bundle of the release instead of a key: --updater-checksums-url (-UpdaterChecksumsUrl) downloads checksums.txt and checksums.txt.sigstore.json (--updater-bundle-url / -UpdaterBundleUrl for another location), verifies the bundle against the identity release.yml@refs/tags/v<version> of this repository with the issuer https://token.actions.githubusercontent.com, and takes the hash of the updater from the verified file. The version comes from --updater-version (-UpdaterVersion) or from the file name myriarch-updater_<version>_<os>_<arch> of --updater-url; if both are given they must agree (option_conflict). New error codes option_removed and sigstore_verifier_outdated. --updater-sha256 stays for hosts without a verifier.
  • Language guard. make check-local runs the language guard strictly for cmd, internal, scripts, ci, contrib, deploy, examples, web/src, docs, .github and the root Markdown files. LANGUAGE_GUARD=warn makes it report-only and LANGUAGE_GUARD=off skips it.
  • New top-level configuration section ui with the options ui.defaultLanguage (default en) and ui.languages (default empty, meaning every language the UI ships). Both are validated at startup: language tags must match ^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$, must not repeat, and ui.defaultLanguage must be one of ui.languages when that list is set.
  • The audit entry artifact.prefetch records the resolved platforms (platforms) and where they came from (platformsSource: request, config or connected).
  • The public endpoint GET /api/v1/auth/config returns the new field ui (defaultLanguage, optional languages). It contains no secrets.
  • The UI derives its languages from a locale registry (web/src/i18n/locales.ts); the language menu and the profile settings offer the shipped languages, restricted by ui.languages. Entries of ui.languages may carry region subtags (de-DE); like the browser language and ui.defaultLanguage, they match a shipped language by the full tag or the primary subtag.
  • Bootstrap admin. auth.bootstrapAdmin (enabled, username, passwordHashFile, tokenFile, keepWithOidc) configures a break-glass administrator for the initial setup and emergencies. Credentials come from files only: a bcrypt hash for password sign-in, a random token for one-time token sign-in (single use, cluster-wide marker, rotated by rewriting the file). The account is active unless OIDC is enabled without keepWithOidc; the configuration is the only source of truth, and PATCH /users/{id} on the account answers 412. Sign-in is rate limited per client address (IPv6 per /64) and per account (per replica, one verification at a time), deactivating the account revokes the API tokens it created, every attempt is audited, and the UI shows a banner on every page while such a session is open. The revocation converges across restarts: the steps of an account run in one transaction, every start removes what an already disabled account still holds (auth.bootstrap.deactivate with alreadyDisabled: true), and an account that becomes active again loses the sessions and tokens of its earlier life before it is enabled (counted in auth.bootstrap.activate). The account backoff is shared by every client, so network access to POST /api/v1/auth/bootstrap-login should be restricted; restarting the pod clears the counter. See docs/operations/authentication.md.
  • IPv6 clients. The failure limiters of the invalid bearer tokens and of the dev login count IPv6 clients per /64 prefix, like the bootstrap sign-in.
  • AWX API prefix. spec.awx.apiPrefix selects the API path of an AWX or Ansible Automation Platform executor (default /api/v2/; Ansible Automation Platform 2.5 and later: /api/controller/v2/). myrctl validate and the syncer check the value (executor_invalid).
  • OpenShift. openshift.enabled (true, false or auto) switches the chart to an OpenShift profile: no fixed UID, GID or fsGroup, a Route (openshift.route.*), the router namespace in the network policy and the OpenShift DNS pods. auto detects security.openshift.io/v1; helm template without --api-versions security.openshift.io/v1 renders the standard profile.
  • Helm chart. examples/quickstart-dex.yaml with examples/dex/dex.yaml (Dex as demo identity provider, OIDC enabled, bootstrap admin off); resources set ephemeral-storage requests and limits; the PodDisruptionBudget renders only with replicaCount > 1; secretChecksum.enabled (default) rolls the pods when a mounted Secret changes (checksum/secret, derived from the uid and resourceVersion of the Secrets, not from their content); helm template fails when neither OIDC nor the bootstrap admin is enabled; the egress rule includes the Git SSH port (22, or the port of an ssh:// URL) when GitOps uses SSH, networkPolicy.egress.extraPorts and extraRules extend it, and networkPolicy.egress.dns sets the DNS rule; networkPolicy.ingress.allowSameNamespace and ingress.allowWithoutTls / allowWithoutTrustedProxies make the safety checks explicit.
  • myriarchd auth hash-password [--cost N] reads a password from standard input (12 to 72 bytes, cost 10 to 14, default 12) and prints a bcrypt hash for auth.bootstrapAdmin.passwordHashFile.
  • Declarative enrollment tokens. opamp.enrollmentTokens[] (name, tokenFile, labels, expiresAt) provisions enrollment tokens from files at start. Only the SHA-256 hash and the display prefix are stored; the start is idempotent, a revoked token is never re-activated, and a token that is removed from the configuration is not revoked automatically (a warning names it). A change of labels or expiry reaches all replicas at once. Such tokens show createdBy: config in GET /enrollment-tokens and a badge in the UI.
  • Secret file permissions. Outside Windows the server logs a warning at start for every secret file that other users of the host can read or write (mode bits 0007): the bootstrap admin and enrollment token files, the database DSN file, the OIDC client secret and key files, the TLS key file, the download token secret, the GitOps token, SSH key and webhook secret files, the release tracker token, the artifact mirror credentials, the notification URL and secret files and the registered executor secrets (executors.secrets; other files in executors.secretDir are not checked). Only the file mode is examined, the content is never read. The message names the setting and the path, never the content. Files of features that have a switch (OIDC, GitOps, release tracker) are checked only while the feature is active; notification channels, the artifact mirror credentials and executor secrets have no switch and are checked when they are configured.
  • Vendor-neutral OIDC. auth.oidc.nameClaim (default name) and auth.oidc.emailClaim (default email); auth.oidc.roleMapping.users maps single users by sub:<subject> or email:<address> (the e-mail form requires email_verified: true and emailClaim: email, because the flag covers only that claim); groupsClaim also accepts an object claim and uses its keys (Zitadel project roles). OIDC callbacks reject the reserved subject prefixes local: and bootstrap:.
  • Provider hints. internal/configload/providers.go holds a table with issuer checks and warnings for Microsoft Entra ID, Keycloak, Okta, Dex and Google. docs/operations/authentication.md has recipes for Keycloak, Dex, Microsoft Entra ID, Okta, authentik, Zitadel and Google.
  • API. POST /api/v1/auth/bootstrap-login (operation bootstrapLogin, public). GET /auth/config returns bootstrapPasswordEnabled and bootstrapTokenEnabled, GET /system/info returns features.bootstrapAdmin, and authMethod and the user provider can be bootstrap. PATCH /users/{id} can answer 412.
  • Audit events. auth.bootstrap.login (success and failure with reason, rate limiting with limitedBy), auth.bootstrap.activate and auth.bootstrap.deactivate (actor system); declarative tokens write enrollment_token.create and enrollment_token.update with source: "config". The metric myriarch_auth_logins_total has the method bootstrap.
  • Database. Migration 00005_bootstrap_admin.sql allows the user provider bootstrap (expand only). The latest schema version is 5.
  • Helm chart. The top-level values bootstrapAdmin.* (generated one-time token Secret <fullname>-bootstrap or an existing Secret with a token or a bcrypt hash) and enrollmentTokens[] (generated or existing Secret per token, mounted below /etc/myriarchd/enrollment) render the configuration above. NOTES.txt explains how to read the generated token without printing it. oidcFromEnvironment: true declares that single sign-on is switched on by environment variables (MYRIARCH_AUTH_OIDC_ENABLED in extraEnv), which the chart cannot read: the login path check passes, the bootstrap Secret is mounted only with keepWithOidc, and the config.auth.oidc.*File fields stay rendered.
  • Web UI. The login page offers the bootstrap admin with password or token (collapsed behind “Use the bootstrap admin” next to other methods, expanded when it is the only one); the users list shows the account as managed by configuration.
  • examples/quickstart/ is a small, generic GitOps repository (fleets linux-hosts, windows-hosts, k8s-gateway; site types edge, datacenter, cloud) with a version policy canary-then-all whose maintenance windows use the site-local time zone and UTC, and a config policy config-canary. A README describes it. examples/sim/mixed-fleet.yaml is the matching simulator profile (1000 agents: 400 / 200 / 300 / 100 and 900 myriarch-updaters).
  • deploy/compose/quickstart.yaml: PostgreSQL, myriarchd dev and the simulator from the release image, for an evaluation on one machine (loopback only, well-known logins; the data directory /var/lib/myriarchd is on a named volume).
  • Licensing. The repository has the Apache-2.0 LICENSE, an SPDX header in every source file and a REUSE 3.3 configuration (REUSE.toml, LICENSES/); the gate licenses of make check-local checks the headers, runs reuse lint and detects drift of THIRD_PARTY_NOTICES. THIRD_PARTY_NOTICES lists the licenses and notices of the Go modules and web UI packages in the binaries and is generated by scripts/gen-third-party-notices.sh; every distinct license text is printed once and referenced by the components that ship it, and the generator fails when the file exceeds 1,000,000 bytes. It is part of every release archive and container image, myriarchd serves it with LICENSE at /licenses without login, and Settings > System links both. The OpenAPI document, web/package.json and the Helm chart declare Apache-2.0. myriarch-updater licenses [--third-party] prints both texts from the binary, so the bare updater binary carries them; the Helm chart contains LICENSE; the container images set org.opencontainers.image.licenses. The licenses of dependencies with a package list in ci/license-overrides.txt fail closed when a new package of the module is linked.
  • Website. The sources of the project website (myriarch.dev) live in website/: Hugo extended with the Docsy theme, built from docs/ and the root Markdown files without copying them, with offline search, light and dark mode, no third-party requests and no cookies (ADR-0031). make website, make website-serve, make website-preview and make website-check build, serve and check it, and the new gate website of make check-local runs the build, the link check, markdownlint and the checks of website/scripts. Hugo, Dart Sass and lychee are pinned in ci/versions.env with SHA-256 values. The site has a landing page with screenshots of the web UI, the REST API reference, the Helm chart, GitOps template and brand pages, a project section with a FAQ, and placeholder pages for documentation that is still being written. The documentation is grouped as getting started, how-to guides, concepts, reference, operations and security. The license texts of the shipped third-party components are published under /licenses/, and the gate checks that every link into the repository names an existing path.
  • Website. The REST API reference is generated from api/openapi.yaml (make docs-gen, checked for drift): one page per tag and a data types page with the site’s theme, dark mode and search, and no JavaScript. It replaces the Redoc viewer.
  • Website. The mark on the landing page plays a short rollout (control plane, canary, waves, full fleet), holds the unchanged mark for about 7 s, retracts the dots briefly and plays again. A click or tap on the mark pauses the loop and shows the static mark, another click resumes it (WCAG 2.2.2; there is no visible button, the mark is the control, also with Enter and Space and a focus ring for the keyboard); the loop also stands still while the hero is out of view or the tab is hidden. With reduced motion the mark is static and not a control; without JavaScript the rollout plays once.
  • Website. .github/workflows/website.yml builds the site with the pinned tools, runs the website gate and deploys website/public as prebuilt static output to Vercel: production on pushes to main, protected previews (noindex) for pull requests from branches of the repository, never for forks. WEBSITE_ENV=preview builds with the production asset pipeline (minified, fingerprinted, no source maps) and only adds noindex and Disallow: /; previews use WEBSITE_BASEURL=/. A new check, check-indexing.py, verifies both modes. The setup (Vercel project, the environments website-preview and website-production, domain records) is in website/README.md.

Security

Fixes and hardening from the pre-publication security audit. The controls are described in docs/security/model.md; the limitations that remain are in its section “Known limitations”.

  • GitOps source. The clone always follows gitops.repoUrl: a changed URL with the same gitops.cloneDir no longer keeps applying the previous repository, and an existing clone whose origin differs is replaced.
  • HTTP listeners. Every request has 60 seconds to deliver its body and an idle write timeout of 60 seconds (accepted event streams and handed-over WebSocket connections are exempt; a rejected request keeps both budgets), request headers are limited to 64 KiB on both listeners, /readyz caches the database check for 2 seconds and coalesces concurrent checks, and public state-changing routes (sign-in) require Content-Type: application/json.
  • Agent authentication. The OpAMP connect limit uses one bucket per IPv6 /64 (IPv4-mapped addresses count as IPv4). Only well-formed enrollment tokens are looked up; unknown tokens are cached in a separate small cache, concurrent lookups are coalesced and lookups of uncached tokens are limited per replica (429 with Retry-After). Token usage is written with one statement per token and flush.
  • Sign-in. Failed OIDC callbacks are limited per client address (both OIDC endpoints refuse an address with an empty bucket), failures before a verified login state are audited at most once per minute and address, and responses of the identity provider are limited to 1 MiB. Group names are matched exactly (no case or Unicode folding) and e-mail entries ignoring the case of ASCII letters only: a mapping that relied on a different case of a group name has to use the exact name. The session cookie is named __Host-myriarch_session where cookies are Secure (production); existing sessions end with the upgrade. API tokens of local development accounts are refused where the development login is off.
  • API tokens. New setting auth.apiTokenMaxTtl (default 90 days; 0 means unlimited and logs a warning): a token created without expiresAt gets the maximum, a later expiresAt is refused with 422, and older tokens stop working. Existing tokens without an expiry stop working 90 days after their creation. The token list and the web UI show the effective expiry. Offboarding a user at the identity provider does not end API tokens: disable the user in myriarch.
  • Configuration checks. Production refuses server.trustedProxies ranges that cover every address and a zero artifacts.downloadTokenTtl, and refuses unanchored artifacts.verify.certificateIdentityRegexp patterns. It warns about /metrics on the main listener, a remote database connection without certificate verification, empty artifacts.dir and gitops.cloneDir, secrets set as literal values in the configuration file, credentials in artifacts.proxyUrl and very broad trusted proxy ranges. myriarchd config print redacts credentials in URL-valued settings. The development mode guard also covers server.metricsListenAddr. The removed constant config.DefaultDataDir was unused.
  • Outbound policy. Only the exact proxy address is exempt from the address check and only in the transport that uses it (ALL_PROXY exempts nothing), more special-purpose ranges are blocked (IPv4-compatible IPv6, Teredo, the discard prefix, 192.88.99.0/24 and the IPv4 documentation ranges; outbound.allowedCidrs can open them), and address literals and localhost in a request URL are refused locally when a forward proxy applies.
  • Secret files. The start-up check also covers files below executors.secretDir that are bound in executors.secretFileHosts and the directory itself, and operator-chosen executor secret files are read as regular files of at most 1 MiB. myriarch-updater warns about loose modes of its token file.
  • Error responses. Database errors that become 4xx answers no longer carry the operation name, constraint names or driver text; they are logged with the request ID.
  • Installers and units. The certificate identities passed to cosign and verify-bundle are bound to the requested version. The Linux installer requires curl unless --insecure is given, extracts archives without owner and mode, and copies --ca-file to /etc/myriarch/ca.pem (the configuration files now reference that path; the option takes an absolute path). The Windows installer prefers the verified updater over cosign.exe from PATH, creates its work directory with a protected DACL, hashes verified files again after copying, and applies and verifies a fresh ACL on the service key before it writes the token (Set-ServiceEnvironment.ps1 does the same). otelcol-supervisor.service drops all capabilities and sets further hardening directives. Both updater and verify-bundle refuse unanchored identity patterns.
  • Installers verify the keyless release bundle (breaking). --signing-key, --updater-checksums-sig-url, -SigningKey and -UpdaterChecksumsSigUrl and the key-signed checksums.txt.sig were removed; using one fails with option_removed, and the message names the replacement (--updater-checksums-url with its bundle, --updater-bundle-url). The error codes signature_invalid, signature_malformed, signature_verifier_missing and signing_key_invalid no longer exist. The verifier of checksums.txt is cosign 3.0.4 or newer or an already installed myriarch-updater, and never the updater that is being installed; an older cosign stops the installation with sigstore_verifier_outdated, also for the collector and supervisor bundles. --updater-checksums-url cannot be combined with --verify-signatures skip. A -Verifier of the Windows installer must be in a directory that only administrators can write (directory_unsafe otherwise), a pre-release of cosign 3.0.4 counts as older, and on Linux every --verifier whose file name starts with cosign gets the version check. -UpdaterSignerThumbprint stays for private PKIs (a pinned thumbprint breaks at every certificate renewal). Known limitation: under App Control script enforcement the unsigned install-windows.ps1 runs in Constrained Language Mode and fails; allow it by its hash.
  • Kubernetes. The collector manifests set automountServiceAccountToken: false, the node agent changes only the top storage directory, and the Helm chart refuses to render inline secrets (config.database.dsn, config.auth.oidc.clientSecret, config.gitops.webhookSecret, config.notifications.channels[].url). The examples use sslmode=verify-full.
  • Dependencies and gates. Go 1.27.2 and golang.org/x/net 0.60.0 fix the reachable vulnerabilities reported by govulncheck. New gates vulncheck (with an allowlist that records reason and review date) and secrets (gitleaks, .gitleaks.toml) in make check-local; the go gate also vets for Windows and compiles the updater tests for it. The security tests of the updater now compile for Windows.
  • Build and release. The collector image defaults to the reviewed upstream digests and verifies the upstream binaries by default when it pushes, the dev image variant is refused for ghcr.io/myriarch/*, and the Docker build contexts exclude every git-ignored path.
  • Verification before publication. ci/release/verify.sh checks the files from which the draft release is created before anyone can publish it: the 42 files, the keyless signature of checksums.txt against the exact identity (tag, commit, workflow, trigger), the ADR-0029 pattern and the version-bound pattern of the installers, the build provenance of all 39 listed files, and for both images the index, the platform manifests and configs (user 65532, one layer, labels), the signatures (a cosign signature of the digest, not only an attestation of the same identity), the provenance and SBOM attestations and the tag list. ci/release/finalize.sh binds the draft to that result through GitHub’s SHA-256 asset digests, refuses unless both packages are readable without credentials and the tag still points at the tagged commit (tag_moved), plans all image tags before it sets one (an immutable tag is never moved, latest never exists), reads the draft by ID again and repeats the checks directly before it publishes, compares the published, immutable release with the verified files once more (release_assets_changed), and checks that the release carries GitHub’s release attestation.

Build

  • Release images. ghcr.io/myriarch/myriarch (myriarchd, myrctl, myriarch-sim and the example repositories) and ghcr.io/myriarch/myriarch-updater are image indexes for linux/amd64 and linux/arm64, built FROM scratch with one layer from the linux archives of the release build instead of from a second compilation. They run as user and group 65532, hold the license files under /usr/share/licenses/myriarch/ and carry the OCI labels in the platform configs and the index annotations. IMAGE_SOURCE=dist IMAGE_KIND=server|updater scripts/build-image-crane.sh writes a deterministic OCI layout (the same archives give the same uncompressed layers, and the same digests with the same zlib) and one SPDX SBOM per platform; ci/release/push-images.sh pushes a layout by digest, never sets a tag and verifies the digest it pushed. See deploy/README.md.
  • Server image content and data directory. The published server image carries myriarchd (entrypoint), myrctl and myriarch-sim in /usr/local/bin and the example repositories /opt/myriarch/examples/quickstart and /opt/myriarch/examples/sim, so a Compose quickstart or a first try needs no repository checkout. All of it is read from the release archives and covered by checksums.txt: the myriarchd archive now also holds examples/quickstart, the builder takes myrctl and myriarch-sim from their own archives, and it refuses archives whose license files differ. The image creates the empty data directory /var/lib/myriarchd (0750, owned by 65532:65532), so a named volume, an emptyDir or a PVC mounted there works for the non-root user and the artifact cache survives restarts. deploy/docker/myriarchd.Dockerfile and the image built from source follow the same contract, and the development Compose file mounts a named volume at that path. The updater image needs no state directory (container mode keeps it on the volume of the collector image).
  • Image user. The image that scripts/build-image-crane.sh builds from the work tree runs as user and group 65532 (it was 1001), like the Helm chart. It refuses ghcr.io/myriarch/*: those repositories are written only by the release workflow.
  • Updater trust store. myriarch-updater embeds the Mozilla root CAs as a fallback, like the other binaries, so that the scratch-based updater image can verify TLS servers. The host trust store still wins.
  • Removed images. deploy/docker/myriarchd-release.Dockerfile and deploy/docker/myrctl.Dockerfile are gone, together with MYRCTL_BASE_IMAGE. The project publishes no myrctl image; the binary is in the release archives, and the GitLab starter in gitops-template/ says to build an image from it.
  • Collector image. deploy/k8s/collector/Dockerfile can copy myriarch-updater from the published updater image (--build-arg UPDATER_SOURCE=image --build-arg UPDATER_IMAGE=ghcr.io/myriarch/myriarch-updater@sha256:<digest>). Container mode is beta and needs a self-built collector image until the collector chart of v0.2.0 (docs/design/container-mode.md, ROADMAP.md).
  • No signing key in the release build. goreleaser signs nothing (no signs: block), ci/build-release.sh has only the snapshot mode, and deploy/release/authenticode.sh is gone. Releases are signed keyless by the release workflow (ADR-0023). The format contract of key-based signatures and the key handling moved to contrib/enterprise-mirror/signing.md for organizations that mirror and re-sign releases.
  • SBOMs. The release build writes an SPDX JSON SBOM per archive (<archive>.sbom.json) and one for the web UI (myriarch-webui_<version>.sbom.json, production dependencies only), made with the pinned syft without network access (syft’s temporary extraction directory is not listed). checksums.txt lists them, and also LICENSE and THIRD_PARTY_NOTICES (39 entries for a release).
  • Windows version resources. myriarch-updater.exe and myrctl.exe carry version information (product “Myriarch”, file and product version from the tag, original file name), generated with the pinned go-winres.
  • Windows signing hook. The release build prepares Authenticode for the Windows files in stages (MYRIARCH_WINDOWS_STAGE): the unsigned files are collected, ci/release/windows-sign-hook.sh is the contract with a signing provider, and the packaging step accepts a signed file only if its signature, attached to the job’s own reproducible build, verifies against the committed chain, subject and RFC 3161 timestamp. A release build has to know the configured signing mode (MYRIARCH_WINDOWS_SIGNING) and refuses a hook result of another provider. make release-rehearsal walks the whole path in a snapshot with a throwaway test CA, and CI runs it. osslsigncode is built from a pinned source archive (ci/install-tools.sh osslsigncode). The Authenticode slot is described in ADR-0034.
  • The Linux updater archives no longer contain the Windows scripts (install-windows.ps1, Set-ServiceEnvironment.ps1); they are in the Windows archives only.
  • Release builds omit the devpg build tag, so the embedded PostgreSQL (and its dependencies) is not linked. myriarchd dev in such a build needs --dsn; make dev and make build keep the tag. See docs/development.md.
  • ci/check-release-licenses.sh (part of make check-local) runs go-licenses over the release builds against a license allowlist; exceptions are documented in docs/development.md.
  • The UI uses system font stacks instead of bundled web fonts, so the embedded UI ships no font files.
  • IMAGE_VARIANT=release scripts/build-image-crane.sh builds the server image without devpg and without the OpenTelemetry release fixtures (/opt/myriarch/release-fixtures); the default dev variant keeps both.
  • The in-process LRU caches use the internal internal/lru package, so github.com/hashicorp/golang-lru is not a dependency.
  • Continuous integration. GitHub Actions runs the gates of ci/check-local.sh as jobs (.github/workflows/ci.yml), with a platform matrix for the Go tests (Linux amd64 and arm64 with a PostgreSQL service and -race, macOS with -race, Windows without -race), the release rehearsal, a secret scan of the commits of a pull request and the script tests. The aggregate job ci-ok is the only required check. .github/workflows/e2e.yml runs the end-to-end smoke test on pull requests and the load run and the UI version update test nightly. Only GitHub-owned actions pinned to a commit SHA are used, the token is read-only and the CI jobs have no secret; a deployment job can read an environment secret only when ci/workflow-secrets.txt lists it, the job declares an environment: and the secret is passed as KEY: ${{ secrets.NAME }} in an env: (ADR-0032).
  • Workflows gate. make check-local has a gate workflows: actionlint (pinned in ci/tools/actionlint) and the policy check ci/check-workflows.py, which also verifies that every gate has a CI job and reads workflow files and local actions with the extension .yml as well as .yaml. The gates go (vet for macOS) and helm (schemas of Kubernetes 1.30.0 and 1.34.12) check more; GO_TEST_RACE, GO_TEST_DEVPG, GO_TEST_PERF, HELM_KUBERNETES_VERSIONS and CHECK_LOCAL_STREAM select variants. The render budget of the server (5000 agents in under 5 s) is asserted only by a run of its own without -race (PERFTEST=1, GO_TEST_PERF), not inside the parallel go test ./..., and applies to the median of three renders.
  • Workflow policy for Scorecard and releases. The policy check allows one third-party action, ossf/scorecard-action, pinned to a commit SHA and only in .github/workflows/scorecard.yml, job analysis (rule R1). Rule R12 holds that workflow to the shape the OpenSSF Scorecard API requires for published results (push to main and a schedule, one job, only uses: steps from a fixed list, exactly the permissions contents: read, actions: read, security-events: write and id-token: write, no secrets or caches). Rule R13 prepares .github/workflows/release.yml: a trigger for pushed v* tags only, no caches or local actions, the literal environment release, a guard job that every job needs, no Node.js, Go, pnpm, npm, goreleaser, make or similar build tool in a job with a write scope, id-token, attestations or an environment, no if: that tests job status and no continue-on-error that would defeat needs:, no script on standard input or foreign shell: in a privileged job, and an approval before every privileged job (ADR-0033, which amends ADR-0032).
  • CodeQL, Scorecard and zizmor. .github/workflows/codeql.yml runs CodeQL (security-extended) on pushes to main, on every pull request to main (forks and Dependabot included) and weekly. It analyses Go (a traced build for Linux, Windows and macOS, also with the embedui and devpg tags), JavaScript and TypeScript, Python and the workflows themselves. .github/workflows/scorecard.yml runs the OpenSSF Scorecard weekly and on pushes to main, publishes the result and uploads the findings to code scanning, in the shape that rule R12 requires. The write scopes of both jobs are reviewed in ci/workflow-permissions.txt. The workflows gate runs zizmor with the pedantic persona and zizmor.yml: offline in the local gate, with the online audits against github.com only in CI, and a file that zizmor cannot parse fails the gate. The new rule R14 of the policy check allows only ignore entries with a worded reason in zizmor.yml and in inline comments, and it rejects files that the check and a YAML parser would read differently (U+0085, U+2028, U+2029, control characters, a byte order mark, text that is not UTF-8). Dependabot now waits 7 days after a release before it proposes a version update; security updates are not delayed (the security model, docs/development.md).
  • Toolchain files. .node-version pins the Node.js LTS release (24.21.0) for CI and local development; the engines of web/ require Node.js 22.22.2, 24.15 or newer (^22.22.2 || ^24.15.0 || >=26). pnpm format:check (prettier) is part of the web gate and make lint, and playwright-core is a development dependency of web/ for the UI smoke tests.
  • Dependency updates. Dependabot (.github/dependabot.yml) now proposes weekly grouped updates for the GitHub Actions, the root Go module, the packages of web/ and website/ and the brand generator (assets/brand/source), and security updates for the CI tool modules (ci/tools/*); docs/development.md, “Dependency updates”, says what each kind of pull request needs besides the update itself (the required DCO check passes Dependabot’s commits as they are). The pins behind the open alerts are raised: Pillow 12.3.0 for the brand generator, which now needs Python 3.10 or newer (BRAND_PYTHON selects the interpreter), and the vulnerable packages of the CI tool modules and the website tooling. The stamp of a Go tool in .cache/bin now holds a digest of its go.mod and go.sum, so a raised requirement rebuilds the tool, also from a tool cache that CI restored from an older run (before, the old binary stayed until the tool version changed).
  • End-to-end test with the real supervisor. E2E_REAL=1 scripts/e2e.sh downloads the upstream supervisor and otelcol-contrib of OTELCOL_VERSION for darwin and linux on amd64 and arm64 (previously darwin/arm64 only) and verifies both against the published SHA-256 on every run. With E2E_REAL=1 or E2E_UI=1 a missing prerequisite fails the run instead of skipping the step. E2E_REAL_BASE_URL replaces the download location.
  • Secret scan of commit ranges. SECRETS_SCAN_RANGE=<base>..<head> ci/secrets-scan.sh scans the commits of a range with gitleaks.
  • Version source. The file VERSION holds the next release tag without the v (for example 0.1.0-rc.3), and scripts/set-version.sh <version> writes it together with every copy: the chart version and appVersion, web/package.json, api/openapi.yaml, the image tags of the production example and of the GitOps template, and the default image tag of the Compose quickstart; then it runs make docs-gen and pnpm gen:api. ci/check-versions.sh fails on drift (part of the go gate and ci/check.sh).
  • Release tag guard. ci/release/guard.sh refuses a release tag that is malformed, lightweight, not on main, different from VERSION or one of its copies, without a CHANGELOG.md heading and entries (final tags) or without entries under “Unreleased” (rc tags), without a successful ci-ok check run, or against the Windows signing rule of deploy/release/windows-signing.env (a final tag with signing off needs the reviewed exception for exactly that tag).
  • Authenticode for Windows. ADR-0034 decides how the Windows release files get Authenticode signatures: the SignPath Foundation is the target, an IV certificate with an HSM key in Google Cloud is the fallback (trigger, budget and privacy consequence are recorded), and the release workflow keeps no long-lived signing secret. Authenticode adds to the Sigstore signature of ADR-0023 and never replaces it. The Windows files of the v0.1.0 release candidates are unsigned (candidates may ship with signing off), and those of v0.1.0 under a reviewed exception for exactly that tag; verify them with cosign before you run them (the security model).
  • Release scripts. ci/release holds the scripts of the release workflow (ADR-0035): meta.sh (the outputs of the guard job; it refuses a run whose GITHUB_WORKFLOW_REF is not release.yml of the project at the tag, which keeps the signing identity frozen), artifact.sh (one deterministic tar per artifact, unpacked only after its SHA-256 matches), tools.sh (builds the signing and the build tool set, and checks a set after the unpack), stage-assets.sh and verify-dist.sh, images.sh, registry-login.sh, verify-blob.sh (the one place that composes cosign verify-blob), sign-checksums.sh, sign-image.sh, notes.sh, draft.sh, verify.sh and finalize.sh. ci/release/identity.env holds the signing identity as data, and ci/tests/release-scripts-test.sh pins it to internal/brand, ADR-0029, the installers and push-images.sh.
  • Release asset list. deploy/release/assets.json is the complete set of release files: 42 names, 40 built (ci/release/stage-assets.sh and verify-dist.sh check them against the goreleaser output; make release-rehearsal does this on every pull request) and two added by the signing job (checksums.txt.sigstore.json and myriarch_<version>_provenance.intoto.jsonl). checksums.txt lists exactly 39 of them.
  • Release notes. ci/release/notes.sh renders deploy/release/notes.md.tmpl and the matching CHANGELOG.md section into the notes of a release: release candidate, final, unsigned-Windows and signed variants, the verification commands with the exact identity and the image digests.
  • Release workflow. .github/workflows/release.yml builds, signs, verifies and publishes a release for every pushed v* tag: build code runs only in jobs without write scopes, the tools come as two sets (cosign, crane and gh are built first on a runner of their own and are the only tools of the privileged jobs), jobs exchange artifacts by ID with a SHA-256 check, two approvals of the environment release gate the signing slot and the publication, and smoke tests of the published release run in the same workflow (ADR-0035). ci/tests/release-workflow-test.sh pins its shape.
  • Smoke tests after publication. ci/release/smoke.sh (subcommands assets, linux, image-run and kind) and ci/release/smoke-windows.ps1 (modes Assets and Install) check a release the way a user does: anonymous downloads of the 42 release files and anonymous registry reads, the keyless signature of checksums.txt (exact and ADR-0029 identity, with a tampered copy as negative control), GitHub’s release attestation, the build provenance and the image attestations, the installers from the verified archives in staging mode with the real collector, docker run of both images (user 65532, no shell, read-only), the Helm chart of the tag on a kind cluster (non-root pods, /healthz, /readyz, bootstrap sign-in) and the Windows files (version resources, Authenticode state, zip contents). The release workflow runs them after finalize (ADR-0035); ci/tests/release-smoke-test.sh tests them with stubs.
  • Enterprise mirror toolkit, keyless mode. contrib/enterprise-mirror/verify-release.sh --keyless --version X.Y.Z --dir|--base-url ... verifies the keyless bundle checksums.txt.sigstore.json of the project’s release with cosign 3.0.4 or newer against the exact identity of release.yml at the tag (and optionally every file against checksums.txt; --trusted-root for hosts without access to Sigstore), and publish-mirror.sh --keyless --dir DIR --version X.Y.Z verifies a downloaded release and uploads the host files unchanged, checksums.txt and the bundle last. The key-based mode is unchanged; --keyless and --public-key exclude each other.
  • Release fixture. The keyless bundle of checksums.txt of v0.1.0-rc.3 is a test fixture (internal/updater/testdata/sigstore/myriarch_0.1.0-rc.3_*): myriarch-updater verify-bundle, install-linux.sh and install-windows.ps1 are tested offline, with the pinned trusted root, against the genuine release signature (accepted for 0.1.0-rc.3; refused for 0.1.0-rc.2, 0.1.0 and a tampered copy), and the real-cosign cases of the installer tests are required in CI.

Documentation

The documentation is English (ADR-0020) and has a new structure under docs/, with Hugo-compatible front matter on every page and a map in docs/_index.md.

  • Concepts (docs/concepts/): GitOps, agents, blueprints, fleets, inventory, rollouts and executors.
  • Tasks (docs/tasks/): step-by-step guides, for example connecting a host, validating a GitOps repository, rolling out a configuration change or a collector version, and simulating a fleet.
  • Getting started and operations: docs/getting-started/local-dev.md, the production checklist and troubleshooting next to the existing authentication guide.
  • Reference (docs/reference/): the GitOps schema reference with every kind, field, template function and issue code, and a command-line reference for all four binaries that make docs-gen generates from the command trees. make docs-check verifies that the generated files are up to date, that links resolve and that the front matter is valid.
  • Generated references for the server configuration (docs/reference/configuration.md), the environment variables and the metrics, produced by make docs-gen from the configuration struct, the loader and the metric definitions; make docs-check fails on drift.
  • Security model (docs/security/model.md): trust boundaries, agent and user authentication, artifact verification, outbound connections and the Kubernetes network policy.
  • Verify releases (docs/security/verify-releases.md, parts 1 and 2): verify checksums.txt with its Sigstore bundle and the exact identity (cosign verify-blob), then the files with sha256sum; verify a downloaded archive before you run its installer, and Unblock-File only after that. docs/tasks/connect-a-host.md, deploy/windows/README.md and contrib/enterprise-mirror/README.md describe the new options, hosts without a verifier, upgrades, and mirrors that carry the keyless bundles unchanged. Part 2 covers the build provenance (gh attestation verify with --signer-workflow, --source-ref and --deny-self-hosted-runners, online and with the provenance asset), the immutable release (gh release verify, gh release verify-asset), the container images (by digest: cosign verify, gh attestation verify oci://... --bundle-from-oci, the SBOM attestation, the sha256-<hex> tags, pinning the digest in Helm and Compose) and the unsigned macOS binaries.
  • Architecture and decisions: docs/architecture.md and the decision records ADR-0001 to ADR-0035 in docs/adr/. ROADMAP.md holds the roadmap.
  • Developer guide (docs/development.md) covers the server, the web UI and the documentation tooling.
  • Community files: README.md (rewritten), CONTRIBUTING.md (DCO sign-off, AI-assisted contributions), CODE_OF_CONDUCT.md, SECURITY.md (private vulnerability reporting), MAINTAINERS.md, GOVERNANCE.md, CONTRIBUTOR_LADDER.md, ADOPTERS.md, ROADMAP.md, AGENTS.md (guidance for coding agents) and CLAUDE.md (a pointer to AGENTS.md), plus issue forms, a pull request template, CODEOWNERS and release note categories in .github/.
  • Tutorial docs/getting-started/first-fleet-and-rollout.md: write a small GitOps directory with a fleet and a rollout policy, enroll ten simulated collectors with an enrollment token and roll out a configuration change in two waves with an approval, all from source and without containers.
  • Tutorial docs/getting-started/docker-compose.md: start the server, PostgreSQL and 200 simulated collectors from the release images with deploy/compose/quickstart.yaml, use your own GitOps directory, and reset; it states prominently that the setup is dev mode and never a production setup.
  • Guide docs/operations/install-helm.md: install the experimental Helm chart from a release tag, with the minimal values, the first sign-in, a GitOps repository, an HTTPRoute and a declared enrollment token, and how to build and use your own image without a container runtime. The values file of the guide is deploy/helm/myriarch/examples/install-guide.yaml; ci/helm-conform.sh renders and checks it.
  • The language guard of make check-local also covers docs/, .github/ and the root Markdown files.
  • Releasing runbook (docs/releasing.md): the job graph of the release workflow, the frozen signing identity, the settings a maintainer makes once (release immutability, the environment release, tag ruleset, packages), releasing a candidate and a final version with the two approvals, the review checklist for the draft, making the packages public after the first push, rc numbering and re-runs, what to do when a job fails, and what never to do. ADR-0035 records the decision.
  • Release build reference (deploy/release/README.md): the 42 assets and assets.json, how the release workflow uses the build, and the release notes template with its placeholders; docs/development.md has a subsection “Release workflow” (how to test it, how to add a job under R13, never rename release.yml).