Decision record
AD-0013 Telemetry disabled by environment baked into the image
Status: accepted, amended twice, once when the image was built and once when the check session was added. Date: 2026-08-27.
Amendment, 2026-08-29: the decision as first accepted named four ENV lines
for the image and left PROMETHEUS_ENABLED=false to the router child’s
environment alone. The scrape endpoint therefore stayed open for anyone who ran
the router binary out of the image directly. The image now carries that
variable as a fifth ENV line beside the four, and it is no part of SR-03.
Amendment, 2026-09-13: the router’s telemetry stays off by environment. When
the operator names a router configuration file in
SYSML_FEDERATION_ROUTER_CONFIG_PATH, the supervisor hands it to the child as
CONFIG_PATH. The file then governs the router’s telemetry, because the
router’s own rule makes a file’s values win over the environment’s. The check
session uses that to send the router’s traces to a collector beside the demo
(AD-0031). With the variable unset nothing changes.
Context
The README says that the router can be started from a pre-built configuration
file with no network dependency at all, and presents that as what makes the
stack air-gappable for defence, rail, energy and medical device work. As the
router ships, the sentence is false. Since router 0.215.0 an anonymous usage
tracker is on by default and is gated on neither the graph token nor the
static execution config. It is switched off only by DO_NOT_TRACK=1
or COSMO_TELEMETRY_DISABLED=true in the environment, or by a Go option that
a router run as a child process cannot reach. There is no YAML key, no
documentation page mentions it, and the only mention in the vendor’s
repository is router/.env.example.
Two further settings matter less. telemetry.tracing.enabled and
telemetry.metrics.otlp.enabled both default to true, with default exporters
pointing at the vendor’s collector when none is configured. With an empty
token the router itself marks those exporters disabled, so setting
TRACING_ENABLED and METRICS_OTLP_ENABLED to false is belt and braces
rather than a requirement.
Whether a router with all of these set opens no outbound connection at all was inferred rather than observed. It was not confirmed by running the image with networking removed. Failures to reach the tracker are debug-logged and non-fatal, so the router reaches readiness whether or not it is trying to reach it, and readiness therefore proves nothing about attempts.
The router runs as a child process from the copied vendor binary, driven by configuration and environment alone (AD-0010). The variables are set in the image. In the README, the air-gap sentence gains the qualification and names the image’s variables. What remained to record is the mechanism and where it lives.
Decision
We will disable every outbound path of the router by setting DO_NOT_TRACK=1,
COSMO_TELEMETRY_DISABLED=true, TRACING_ENABLED=false and
METRICS_OTLP_ENABLED=false as ENV instructions in the Dockerfile. That way
the variables are part of the image rather than of any launch command. We will
set no graph token, so that the router’s own token check disables the Cosmo
Cloud exporters, schema usage tracking and persisted operations as well. The
supervisor passes the four variables into the router child’s environment beside
LISTEN_ADDR, PLAYGROUND_PATH and EXECUTION_CONFIG_FILE_PATH, and SR-03
states them as an obligation on the image and the router rather than as a
rationale.
A fifth ENV line sits beside those four, so the Dockerfile carries five.
PROMETHEUS_ENABLED=false closes the scrape endpoint the router opens on
loopback by default, which the supervisor also closes in the child’s
environment. It is a listener rather than an outbound path, so it is no part of
SR-03. Its place in the image is for anyone who runs the router binary out of
the image directly, where nothing builds an environment on their behalf.
Alternatives considered
Relying on the empty token alone, with the vendor’s defaults otherwise in place. That disables the default OTLP exporters and the control plane self-registration but leaves the usage tracker running, so the air-gap claim would be false as shipped.
A key in the router’s configuration file. None exists for the usage tracker, so a YAML file could not carry the setting even if the image shipped one.
The Go option WithDisableUsageTracking(). It is reachable only from a router
embedded as a library, which AD-0010 rejected on the module’s pseudo-versions,
its required replace block and its two API breaks.
Supplying the variables on the launch command. The launch line is one
docker run with one flag, and a variable the visitor has to remember is one
that will be forgotten, which is what baking them into the image rules out.
Consequences
The container makes no outbound connection while it runs, which is what the context view states and SR-03 requires, and the README sentence can be qualified rather than withdrawn. All four names appear in the Dockerfile, in the supervisor’s environment for the child and in the public text that describes static composition, so an organisation building its own image around the adapter knows what to set.
The router also serves a Prometheus scrape endpoint, on 127.0.0.1:8088 unless
it is told otherwise. That is a different mechanism from the exporters this
record disables, since a scrape endpoint waits to be read and opens nothing
outbound, so the air-gap claim never rested on it either way. It is switched off
by PROMETHEUS_ENABLED=false in the child’s environment and again in the
image’s ENV block, because nothing here reads it, and a port with no reader is
one more thing a port table has to explain.
The cost is a dependency on variable names the vendor does not document, and
there are three of them: DO_NOT_TRACK, COSMO_TELEMETRY_DISABLED and the
PROMETHEUS_ENABLED above. No documentation page names any of the three, and
the router binary’s help lists no environment variables at all, so a release
that renames one, changes a default or adds a second tracker would announce it
nowhere. Nor would anything here notice. The test that guards SR-03 asserts the
environment the supervisor builds for the child, not what the router does with
it. A name the router has stopped reading therefore passes it unchanged while
the container goes back to reporting usage or to opening a scrape port. The
router version is pinned together with wgc. Every bump means reading the
release notes and router/.env.example for all three names, then starting the
container at debug level and confirming from its log that usage tracking is off
and that nothing listens on 127.0.0.1:8088. AD-0010 sets a fourth
undocumented name for a purpose of its own and carries the same obligation, so
the two are checked in one pass. This is maintenance the demo would not
otherwise carry.
The claim is only as good as its verification, and readiness is not it. SR-03
therefore has three parts. One is a test that the router process’s environment
contains the four variables and that the execution configuration names only
loopback addresses. Another is an analysis of the paths that could open a
connection. The third is a demonstration that runs the container under
docker run --network none at debug log level and watches for connection
attempts. The demonstration is a spike, and it must pass before the air-gap
sentence goes public.
wgc itself sends usage events unless the same two variables are set. Composition is a maintainer step on a connected machine (AD-0012), so that is a habit for the maintainer rather than a property of the image, and this record does not cover it.
Requirements affected
SR-03
Sources
The vendor’s router/.env.example, which is the one place DO_NOT_TRACK and COSMO_TELEMETRY_DISABLED are named. The vendor’s pages on tracing and metrics exporters and on running without a graph token. What the research overturned for the finding, first inferred from the router’s code and later run, and The image in the example’s README for the five ENV lines, and its verification record for the air-gap demonstration.