Talos compatibility: generation, validation, configuration RPCs and support policy¶
| Date | 25 September 2026 |
| Work item | E3: establish PoC Talos compatibility |
| Design reference | §6.5 Renderer and contract pinning, §18.1 Phase 0, §18.2 Phase 1 |
| Artifacts | experiments/e3-talos-compatibility/, evidence under experiments/e3-talos-compatibility/evidence/ |
| Decision enabled | The compatibility boundary a PoC renderer can rely on, and what choosing talosctl as a subprocess or the Go machinery costs. Input to renderer selection. No renderer, fleet version set or support policy is selected here. |
This is not a pass of design experiment E3. The §18.1 row for E3 includes the Upgrade/LifecycleClient transition. Nothing here executed an upgrade, a reboot or any lifecycle RPC; the fixture's Talos nodes are containers, where those operations do not exist (this report's §6.3).
1. Question¶
Design §6.5 separates four things a release records and a renderer must respect: the observed running version, the desired image, the generation contract and the renderer version. It then says that "backward-generation constants, upstream release-support policy and working machine operations are separate axes", and that "the machinery minor being at least the target contract minor is a prerequisite, not a sufficient compatibility test". Issue 5 asks for a tested matrix that keeps those axes apart:
- Generation capability: which contracts a renderer produces, and what it does when asked for one it cannot produce.
- Validation: which validators accept which renderer's output.
- Operation/RPC compatibility: which clients complete the PoC's configuration RPCs against the fixture's live v1.13.6 node, and which generated configurations that node accepts.
- Upstream support policy: what the machinery encodes and what the upstream documentation states.
It also asks whether talosctl as a subprocess and the Go machinery (pkg/machinery) behave the
same on the same cells, and what each implies for the structural-reference evidence of
issue 3.
2. Renderers, targets and the two implementations¶
Six renderers (versions.env), chosen to
cross every boundary the PoC could meet: the previous minor, the first and a later patch of the
fixture's minor, the fixture's own pin, the next minor, and a prerelease.
| Renderer | Why |
|---|---|
| v1.12.12 | previous minor, latest patch |
| v1.13.0 | first release of the fixture's minor |
| v1.13.6 | the fixture's pin (fixtures/versions.env as of c74f953), the reference for every comparison |
| v1.13.10 | a later patch of the fixture's minor, newer than the node |
| v1.14.1 | next minor |
| v1.15.0-alpha.0 | prerelease |
Targets: current (no --talos-version), the contracts v1.10.0 to v1.15.0, and five
version-string edges: 1.13 (no v), v1.13.6 (a patch), v1.15.0-alpha.0 (a prerelease),
v1.99.0 (a minor no renderer knows) and bogus. Kubernetes: each renderer's own default, and the
fixture's 1.36.2.
Every cell runs through both implementations:
- talosctl as a subprocess: that release's
talosctl, downloaded once and checked against the SHA-256 pinned for it inversions.env. The v1.13.6 pin is the fixture's, which the fixtures report records as taken from upstreamsha256sum.txt; the committed evidence does not record where the other five pins came from. - the Go machinery:
e3m, one source (machinery/src/main.go) built once per version against that tag'spkg/machinerymodule.gengoes throughbundle.NewBundleastalosctl gen configdoes;validatecallsConfig.Validatein a runtime mode;version,readandapplyuse the machinery client.
3. What was built¶
3.1 Groups¶
A table row holds several cells: one per implementation, and for validation one per implementation and runtime mode.
| Group | Rows (cells) | What it records |
|---|---|---|
| gen | 144 rows (288 cells) | every renderer x target x Kubernetes setting: both implementations' exit and output, the output digests, whether the two are identical, and whether talosctl repeats itself |
| validate | 504 rows (2016 cells) | every generated configuration at Kubernetes 1.36.2 (7 targets x 2 machine types per renderer) x every renderer as validator; each row's four cells are talosctl and the machinery, each in container and metal mode |
| strict | 84 rows (336 cells) | each renderer's own output with --strict, the same four cells |
| policy | 96 answers per machinery | the Kubernetes and upgrade windows each machinery's compatibility package encodes |
| rpc-client | 12 rows | each renderer as a client of the v1.13.6 worker, through both: version, read, dry-run apply and real apply of a label patch in no-reboot mode |
| rpc-contract | 42 rows | every renderer's worker configuration for current and each contract, as a no-reboot dry run through the pinned client, both implementations |
| controls | 5 rows | positive controls, below |
| tests | 1 row | e3m's own tests |
The gen, validate, strict and policy groups are measurements: the renderer's behaviour is the
finding, and each distinct message is cited once by id
(messages.tsv). The rpc and
control groups are rows with an expectation written in run/all before the run and compared with
what the harness reads back from the worker through the pinned talosctl: the configuration
digest and the machine-configuration resource version.
Controls (all fired in the capture):
| Control | Proves | Observed |
|---|---|---|
gen-bogus-refused |
generation can fail | bogus refused in all 12 gen rows for it (6 renderers x 2 Kubernetes settings), through both implementations in each |
gen-repeatable |
a digest difference is not noise | every one of the 132 generating rows compares same with its talosctl repeat; 0 differ |
compare-unequal |
the comparison can report a difference | a different cluster name compares unequal, and e3m paths names 0/cluster/clusterName |
validate-invalid-refused |
validation can fail | a worker with type: bogus accepted by 0 of 24 validator cells (6 renderers x 2 modes x 2 implementations), and all 24 outputs name unknown machine type "bogus" |
rpc-invalid-refused |
the node can refuse | the same file refused by the node through both clients, each naming unknown machine type "bogus"; the worker's digest and version unchanged |
3.2 Pinned versions¶
| Component | Version |
|---|---|
talosctl |
the six renderers, each SHA-256 in versions.env and tools.tsv |
pkg/machinery |
the same six tags, machinery/<version>/go.mod and go.sum |
| Go | go1.27.1 |
| Live node | Talos v1.13.6 control plane and worker, Kubernetes 1.36.2; image digests in bundle-summary.txt |
| Fixture pins | fixtures/versions.env as of c74f953 |
| Captured from | 2809915, 0 uncommitted inputs (run.txt) |
The machinery defaults Kubernetes to 1.35.8 (v1.12.12), 1.36.0 (v1.13.0), 1.36.2 (v1.13.6), 1.36.3
(v1.13.10) and 1.37.0 (v1.14.1, v1.15.0-alpha.0). Every configuration names the installer image
ghcr.io/siderolabs/installer:v1.13.6, because the renderers' own defaults differ and would make
every cross-renderer comparison unequal for that reason alone.
3.3 Secrets and the leak scan¶
Every configuration is generated from the fixture cluster's own secrets bundle so that it can be
applied to the worker. The configurations, raw outputs and worker reads therefore hold the
fixture's secrets, and stay in E3_OUT outside the checkout. What is committed of a configuration
is its digest and its key-path differences (e3m paths: +/-/~ and a path, never a value).
A transcript keeps an apply's output only up to its first diff line, and a read's standard error
only, with a line count and digest for what was withheld. e3m withholds a diff from its own
errors the same way, printing withheld_lines= instead.
fixtures/bin/evidence ran with E3_OUT as an extra scan path. It matched its planted control,
the pattern list, and the generated and raw configurations in E3_OUT, as expected, and nothing
in the fixture's own state beyond the control. run/collect-evidence then planted its own control,
confirmed it fired, and found 0 hits of the 16 patterns and 0 paths under the operator's home in
the 22 committed files (leak-scan.tsv).
3.4 Reproduction¶
fixtures/bin/up
E3_OUT=<new empty absolute path outside the checkout> experiments/e3-talos-compatibility/run/all
fixtures/bin/evidence <the same path>
cp -a fixtures/.state/evidence/<bundle> <the same path>/bundle
fixtures/bin/down
E3_OUT=<the same path> experiments/e3-talos-compatibility/run/collect-evidence
run/all took 84 seconds with the six talosctl downloads already cached and the fixture up
(started/finished in run.txt). E3_OUT needs about 1 GiB, mostly the tool copies.
4. The matrix¶
The four columns are kept apart. A cell in one column says nothing about the others: an output that validates may still be refused by the node, and a combination the node accepts may be outside upstream support.
4.1 Generation capability¶
Source: gen.tsv.
- talosctl and the machinery produce identical output in all 132 generating rows, and talosctl repeats itself in every one. "Identical" here and below means equal digests after the trailing-newline normalization (the experiment's README).
bogusis refused by every renderer through both (M3, M4:error parsing version "vbogus").- A target newer than the renderer is not refused. The renderer silently produces its own current contract. At Kubernetes 1.36.2, grouping each renderer's outputs by digest:
| Renderer | Identical groups of targets |
|---|---|
| v1.12.12 | {v1.10.0, v1.11.0}; |
| v1.13.0, v1.13.6, v1.13.10 | {v1.10.0, v1.11.0}; {v1.12.0}; {current, v1.13.0, v1.14.0, v1.15.0, 1.13, v1.13.6, v1.15.0-alpha.0, v1.99.0} |
| v1.14.1 | {v1.10.0, v1.11.0}; {v1.12.0}; {v1.13.0, 1.13, v1.13.6}; {current, v1.14.0, v1.15.0, v1.15.0-alpha.0, v1.99.0} |
| v1.15.0-alpha.0 | {v1.10.0, v1.11.0}; {v1.12.0}; {v1.13.0, 1.13, v1.13.6}; {current, v1.14.0, v1.15.0, v1.15.0-alpha.0, v1.99.0} |
So v1.12.12 asked for v1.13.0 returns a v1.12 configuration, exit 0, no warning. 1.13 and
v1.13.6 are the v1.13.0 contract: the patch and the missing v are ignored.
- A given contract's output is the same from every renderer that knows it. Across renderers at
Kubernetes 1.36.2, four distinct outputs cover every contract cell: one for v1.10/v1.11 (all six
renderers), one for v1.12 (all six), one for v1.13 (the five renderers from v1.13.0 on), one for
v1.14/v1.15 (v1.14.1 and v1.15.0-alpha.0). The clamped outputs join the group of the renderer's
own current contract.
- v1.10.0 and v1.11.0 produce identical output in every renderer, for these inputs.
- The prerelease adds nothing. v1.15.0-alpha.0's v1.15.0 and current outputs equal v1.14.1's
v1.14.0 output.
- v1.14 changes the document layout. Against v1.13.6, v1.14.1's worker configuration loses
cluster/*, machine/kubelet, machine/install and two machine/features fields from the
v1alpha1 document and gains 13 documents, among them KubeletConfig, DiscoveryServiceConfig,
KubeClusterConfig and UnattendedInstallConfig
(paths/renderers.txt).
Against the pinned renderer's current worker output, its v1.10/v1.11 contracts add
machine/features/{apidCheckExtKeyUsage,rbac,stableHostname} and machine/network, and lack
machine/install/grubUseUKICmdline and the HostnameConfig document; v1.12 adds only
machine/network
(paths/contracts.txt).
4.2 Validation¶
Source: validate.tsv,
strict.tsv.
| Configuration | v1.12.12 to v1.13.10 validators | v1.14.1, v1.15.0-alpha.0 validators |
|---|---|---|
| any renderer's v1.10 to v1.13 contract, or a pre-v1.14 renderer's clamped output | valid, container and metal, both | valid, both |
v1.14.1 or v1.15.0-alpha.0 at current, v1.14.0 or v1.15.0 |
refused by both: "DiscoveryServiceConfig" "v1alpha1": not registered (M8 to M11) |
valid, both |
- Of 504 rows, 456 are valid in all four cells and 48 are refused in all four (4 validators x
6 configurations x 2 machine types): no row mixes verdicts, so talosctl and the machinery reach
the same verdict in all 2016 cells. The message text is not the same: a valid cell prints M5 or
M6 through talosctl and M7 through
e3m, and a refusal differs bye3m's wrapper (§6.2). Validation was only given talosctl's files, which are identical to the machinery's (§4.1). - v1.14.1 accepts v1.15.0-alpha.0's output, which is the v1.14 layout (§4.1).
- Neither generation nor validation enforces the Kubernetes window. v1.12.12 generates and
validates a worker for Kubernetes 1.36.2 (
gen.tsv,validate.tsv) although its owncompatibilitypackage refuses 1.36.2 for the v1.12 target as too new (policy/v1.12.12.tsv); every renderer's v1.10 contract, whose window ends at 1.33 (§4.4), does the same. Only thecompatibilitypackage refuses such a combination, and only when asked. --strictrefuses nothing: all 84 own-output rows (336 cells) valid, because no renderer emits a warning for its own output.
4.3 Operation/RPC compatibility against v1.13.6¶
Source: rows.tsv, transcripts in
transcripts/.
Clients (rpc-client, 12 of 12 rows match). Every renderer, through both implementations,
reads the node's version (v1.13.6), reads its machine configuration with the same digest as the
harness's own read, completes a no-reboot dry run that leaves the configuration and its version
unchanged, and applies a label patch that the harness then reads back as landed. The three
talosctl clients newer than the node print, on standard error and only for the configuration read
(talosctl get):
(and the same for 1.14.1 and 1.15.0-alpha.0). Their version and apply-config steps print none.
The node returned no apply warnings to the machinery client (apply_warnings=0 in every row). The
machinery module has no skew check of this kind, so e3m prints none by construction; a machinery
caller that wants one has to compare versions itself. Standard output was unaffected, so a
subprocess caller that separates the streams reads the same bytes.
Configurations (rpc-contract, 42 of 42 rows match: both implementations exit alike, both zero or
both non-zero, and nothing lands). Each renderer's worker configuration was dry-run applied to the
live worker in no-reboot mode through the pinned client, as talosctl and as the machinery. The
node's reason is the same text through both in all 18 refused rows, once e3m's quoting of " is
removed. That was checked by reading the reason and mreason fields of rows.tsv, not by the
harness: its verdict compares exit statuses only and records the two reasons without comparing
them (§7).
| Worker configuration | Node's answer |
|---|---|
| v1.12 or v1.13 contract, from any renderer | accepted |
| a pre-v1.14 renderer's clamped v1.14.0/v1.15.0 output (which is its own current contract) | accepted |
v1.14.1 or v1.15.0-alpha.0 at current, v1.14.0 or v1.15.0 |
refused: error decoding document v1alpha1/DiscoveryServiceConfig/default … not registered |
| v1.10 or v1.11 contract, from any renderer | refused: this config change can't be applied in immediate mode |
The v1.10/v1.11 refusal is about the apply mode, not the contract's decodability. That is an
inference from the refusal text and from §4.2, where v1.13.6 validates these files: the node
appears to have decoded the file and declined to apply it without a reboot. Against the live
worker, those contracts differ in the paths above (§4.1) plus certSANs, the nodeLabels left by
the rpc-client rows' label patches, and the generated install fields
(paths/live-worker.txt).
The accepted v1.12 contract and the refused v1.10/v1.11 contracts differ from each other only in
machine/features/{apidCheckExtKeyUsage,rbac,stableHostname}, machine/install/grubUseUKICmdline
and the HostnameConfig document, so the reboot-only change is among those; which one is not
isolated here. Whether they apply with a reboot cannot be measured in containers.
4.4 Upstream support policy¶
What each machinery encodes (compatibility package, policy/):
Kubernetes accepted by KubernetesVersion.SupportedWith, over the points 1.29.0 to 1.37.0:
| Talos target | Kubernetes accepted | Upgrade accepted from (TalosVersion.UpgradeableFrom, over v1.10 to v1.15) |
|---|---|---|
| 1.10 | 1.29 to 1.33 | 1.10, 1.11 |
| 1.11 | 1.29 to 1.34 | 1.10 to 1.12 |
| 1.12 | 1.30 to 1.35 | 1.10 to 1.13 |
| 1.13 | 1.31 to 1.36 | 1.11 to 1.14 |
| 1.14 | 1.32 to 1.37 | 1.12 to 1.15 |
| 1.15 | 1.33 to 1.37 | 1.13 to 1.15 |
- Each machinery that knows a target gives the same answer for it. A machinery that predates the
target has no answer:
compatibility with version 1.14.0 is not supportedandupgrades to version 1.13.0 are not supported. - A bound at a test point's edge (Kubernetes 1.29 for 1.10 and 1.11, 1.37 for 1.14 and 1.15; host 1.10 for 1.10 to 1.12; host 1.15 for 1.14 and 1.15) is the sample's edge, not necessarily the machinery's.
- The "upgrade" window includes newer host versions, that is, downgrades: 1.13 accepts a 1.14
host, and refuses 1.15 as
too new to downgrade.
What the upstream documentation states (read 2026-09-25, no last-updated date on the pages;
the cells and sentences used here are recorded verbatim in
upstream-docs.md):
- The v1.13 support matrix lists Kubernetes 1.31 to 1.36 for 1.13 and 1.30 to 1.35 for 1.12; community support for 1.13 ends at the 1.14.0 release, given as "2026-08-30, TBD".
- The v1.14 support matrix lists Kubernetes 1.33 to 1.37 for 1.14 and gives the 1.14.0 release as 2026-08-27. The machinery accepts 1.32 for 1.14, and the two pages disagree on the 1.14.0 date.
- The upgrade guide says the migration "is only tested between adjacent minor releases" and recommends upgrading through the latest patch of every intermediate minor. For the client, it recommends "the version that matches the current running version of the cluster".
- The older
talos.dev/v1.13/introduction/support-matrix/URL returned 404 when the pages were read. That is not recorded inupstream-docs.mdand is outside the committed evidence (§7).
So the machinery's windows are wider than the documented tested path: it accepts a two-minor upgrade and a one-minor downgrade that the documentation does not describe as tested, and one Kubernetes minor more for 1.14 than its own documentation lists.
5. Failures hit while building it¶
5.1 A harness read died on SIGPIPE¶
The first capture stopped at "could not read the worker's resource version". awk … exit read the
first match and closed its input, and the upstream talosctl then failed with SIGPIPE, which
pipefail turned into a failure. The read now captures the output first and parses it afterwards.
5.2 A stale artifact digest made refusals look like landings¶
The second capture's rpc-contract rows read the worker as is=artifact after a refused dry run: the
previous row's artifact digest was still set. row_begin now clears every per-row digest.
5.3 The skew warning broke the read digest¶
talosctl v1.13.10 and later print the skew warning (§4.3) with the configuration, and a combined
stream no longer matched the harness's digest. Reads now keep standard output as the raw
configuration and standard error in the transcript.
5.4 Error and dry-run output embed the configuration¶
A refused or dry-run apply prints a configuration diff, which holds the fixture's secrets.
Transcripts keep an apply's output up to the first diff line and record the line count and digest
of the rest. That was tested against the raw outputs of the second capture (0 leak-pattern hits);
those outputs were not kept. The pre-draft review then found that e3m printed a refusal's whole
error, diff included, on one line that this filter could not see. e3m now withholds the diff
itself: in the final capture each of the 12 immediate-mode refusals through the machinery withheld
13 lines.
5.5 The first committed bundle manifest was 124 KB¶
The fixture bundle's postgres-data/ alone is over a thousand manifest lines. The committed
manifest lists everything else and gives that directory one line, the digest of its part of the
full manifest, which stays in E3_OUT.
5.6 The harness did not enforce its own controls¶
The pre-draft review found that run/all wrote control mismatches N and finished normally
whatever N was, and that run/collect-evidence never read it. A failed control now makes run/all
exit non-zero, and the collector refuses the run. The same review hardened two controls that could
pass vacuously: repeatability now requires every generating row to compare same, and the invalid-
configuration controls require the refusal to name the planted reason, so that a connection
failure cannot read as a refusal. The capture was repeated at 2809915 with every control firing.
5.7 The packed transcripts failed the whitespace check¶
talosctl version ends its Built: line in spaces. The collector already dropped trailing
whitespace from the tables but not from the packed transcripts, and the first commit of this
capture's evidence failed git diff --check on twelve such lines. The packs now drop it too, and
the evidence was collected again from the same capture; only those twelve lines changed.
6. What this decides¶
6.1 Criterion 1: a matrix with four separate columns¶
§4.1 to §4.4 are the four columns, each from its own evidence file. Stated per axis for the PoC's live v1.13.6 node:
| Renderer | Generation | Validation by v1.13.6 | Node accepts (no-reboot dry run) | Upstream |
|---|---|---|---|---|
| v1.12.12 | v1.10 to v1.12; newer targets clamped to v1.12 | valid | v1.12 contract yes; v1.10/v1.11 not in immediate mode | client one minor older than the node; not the documented recommendation |
| v1.13.0, v1.13.6, v1.13.10 | v1.10 to v1.13; newer clamped to v1.13 | valid | v1.12, v1.13 yes; v1.10/v1.11 not in immediate mode | matches the node's minor |
| v1.14.1 | v1.10 to v1.14; newer clamped to v1.14 | v1.14 output refused | v1.12, v1.13 yes; v1.14 refused; v1.10/v1.11 not in immediate mode | next minor; not the documented recommendation |
| v1.15.0-alpha.0 | as v1.14.1; the v1.15 contract equals v1.14 | as v1.14.1 | as v1.14.1 | prerelease |
The clients' own skew warning (v1.13.10 and later talosctl, on reads only) is an RPC observation
and stays in §4.3.
6.2 Criterion 2: subprocess against machinery, and the structural-reference evidence¶
Behaviour is the same. In every cell measured through both, the two implementations agree:
identical generation (132 of 132 rows), the same validation verdict (504 rows, 2016 cells; strict
84 rows, 336 cells), the same refusal text in the 48 refused validation rows (M8/M9 and M10/M11,
differing only in e3m's error="…" wrapper, its escaped quotes and its withheld_lines=0), the
same RPC outcomes for the 6 client pairs, the same node verdict for all 42 contract dry runs and the
same reason in the 18 refused ones, and the node's refusal of the invalid control. A valid cell's
output is not the same text: talosctl prints M5 or M6, e3m prints M7. The 18 reasons were
compared by reading rows.tsv, not by the harness (§4.3). Two things were not compared: validation
of the machinery's own files (identical to talosctl's, §4.1), and the skew warning, which the
machinery has no code for (§4.3). The choice between them is not a compatibility choice within this
matrix. It is a packaging and maintenance one:
| talosctl subprocess | Go machinery | |
|---|---|---|
| Several renderer versions in one manager | one binary per version (98 to 119 MB each here; not in the committed evidence), checked by digest | one build per version: a Go build links a single version of pkg/machinery, so several versions mean several binaries or plugin processes anyway (the six modules under machinery/) |
| Version-to-version API drift | CLI flags: apply-config --mode lists auto, no-reboot, reboot, staged, try in v1.12.12 to v1.13.10 and drops reboot from v1.14.1 on (talosctl apply-config --help; not in the committed evidence) |
secrets.Bundle.Validate gained a version-contract parameter in v1.14, which needed two shims selected per module (bundle_v112.go, bundle_v114.go) |
| API surface not public | none needed | talosctl validate's --mode type lives in the talos module's internal/ tree; e3m implements the machinery's RuntimeMode interface itself |
| Output handling | separate stdout from stderr (skew warning, §4.3); parse text | a refusal is a gRPC status whose message string embeds the configuration diff, and a dry run's ModeDetails string carries it too; both must be filtered as text, as for talosctl (e3m keeps an error up to its first diff line and only the first line of ModeDetails, §7) |
| Silent clamping (§4.1) | same | same: the caller must enforce "renderer minor ≥ target minor" itself |
Structural references. The
composition evidence (report in
ginsys/bronzeward#42, not yet on main) found that
references must be resolved before native composition, so that talosctl receives only
reference-free fragments, and it measured that on v1.13.6 only. E3 adds:
- Because generation is identical between the two implementations, early resolution's parity
result does not depend on the choice between them for generation at v1.13.6. E3 did not
compose patches through the machinery's
configpatcher; every patch here went through the pinnedtalosctl machineconfig patch, so parity of composition through the machinery is inferred, not measured. - Because a contract's output is identical across every renderer that knows it (§4.1), the issue-3 parity verdicts plausibly hold for the v1.12 and v1.13 contracts from other renderers. That is an inference from generation only; composition on other renderers was not run.
- The v1.14 layout moves many v1alpha1 paths into separate documents (§4.1). A reference addressed by document and path, as issue 3's binding candidate is, would point somewhere else under a v1.14 contract. A contract change is therefore also a reference-address change, and the compiler must version its paths with the contract.
- The machinery returns the same error text as
talosctl(M8/M9, the rpc-contract reasons and the controls; M4'sinvalid talos-version:prefix ise3m's own). Issue 3's observation that decode errors quote a value prefix plausibly holds for both implementations, since the decoder is the machinery's and not the CLI's. That is an inference: no output in this run quotes a value.
6.3 Criterion 3: unsupported combinations, and the deferred lifecycle tests¶
Unsupported, with the observed rationale:
- A contract newer than the renderer. Not refused: silently rendered as the renderer's current contract (§4.1). A manager that trusts the renderer's exit code would record the wrong contract.
- v1.14+ configurations on a pre-v1.14 validator or node. Refused on decode: the
DiscoveryServiceConfigdocument is not registered before v1.14 (§4.2, §4.3). - v1.10/v1.11 contracts applied to a v1.13 node in no-reboot mode. Refused by the node as not applicable in immediate mode (§4.3). With a reboot: not measurable here.
- An unparseable version string (
bogus). Refused by every renderer. - Kubernetes outside a target's
SupportedWithwindow. Generated and validated without complaint (§4.2); only the machinery'scompatibilitypackage refuses it (§4.4). Like item 1, a manager that relies on the renderer would not notice. Not exercised against a node. - Talos targets a machinery predates. The
compatibilitypackage has no answer for them (§4.4). - The v1.15.0-alpha.0 prerelease as a v1.15 renderer. It produces no v1.15-specific output (§4.1), so it cannot stand in for v1.15.0.
Deferred: Upgrade/LifecycleClient transition execution. Not tested. The fixture's Talos nodes
run as containers, where upgrade, reboot, reset, disk and installer behaviour do not exist
(fixtures report §6). The
renderer research records topf switching
from the legacy MachineService.Upgrade below Talos 1.13 to LifecycleClient.Upgrade at or above
it. None of the following was executed: an upgrade through either RPC, the version gate that
chooses between them, an upgrade across the v1.13 boundary, a v1.14 contract applied after an
upgrade to v1.14, any reboot-mode apply, and the §4.4 upgrade windows. Disposition: these
belong to the machine-lifecycle phase (§18.3)
and its acceptance proof (§18.5),
on VMs or hardware rather than containers. The fixtures report names Talos in QEMU as the tool for
them, rejected for Phase 0 because its provisioner requires root
(fixtures report §2). They remain an
unmet part of design experiment E3.
6.4 Criterion 4: no pass claimed¶
Design experiment E3 did not pass. This report establishes a tested boundary for configuration generation, validation and the PoC's configuration RPCs against one live v1.13.6 node. The Upgrade/LifecycleClient transition, which the §18.1 row names, was not executed (§6.3).
7. Limits¶
- One live version. Every RPC result is against Talos v1.13.6. No node ran any other version, so "the node accepts" means "a v1.13.6 node accepts".
- Containers, one control plane and one worker. No reboot, upgrade, reset, installer or disk behaviour; no remote transport; no multi-worker rollout.
- Validation and RPC coverage is narrower than generation. Validation covers Kubernetes 1.36.2
and
currentplus the six contracts, not the version-string edges or the renderers' default Kubernetes versions. The contract dry runs used the worker configuration and the pinned version of each implementation only; only the label patch was really applied. - The contract dry runs ran against the worker as the rpc-client rows left it, carrying their 12 label patches, not against the fixture's initial configuration.
- "A label patch" was applied as a full re-encode. Each rpc-client artifact is
talosctl machineconfig patch's output for the whole configuration, and the worker's configuration read 428 lines at row 001 and 55 lines at row 003, after the first two applies (transcripts/rpc-client.txt). What the re-encode changed beyond the label is not recorded. - The rpc-contract verdict compares exit statuses only.
verdict=agreewhenever both exits are zero or both are non-zero;reasonandmreasonareseen:readings, which only require the reading to exist, andrun/allalways sets both (-when there is none). The same reason through both (§4.3) was read fromrows.tsv, and no control shows adifferverdict firing. e3m's diff filter fails open.withholdDiff(machinery/src/main.go) cuts an error at the first line that starts withdiff:,Config diff:or---, as the harness'sstep_quietdoes for talosctl; a diff in any other format would be printed into a committed transcript, with the leak scan's fixed-string patterns as the only backstop. In this capture no refusal's output held a scan pattern: the leak-scan list inbundle-summary.txtnames none of the 18 refused rows' raw talosctl outputs, while it names all 24 accepted rows'. So the filter was never exercised on a diff holding a secret; its unit test uses a synthetictoken:line. A later implementation should fail closed: withhold everything it cannot positively recognise as safe.- Missing inputs shrink the tables instead of failing the run. An rpc-contract row whose worker
configuration is missing is skipped (
continueinrun/all), andpaths_runwrites no section when either file is missing (return 0); only the validate and strict groups record the gap, asno-config. Thegen-bogus-refusedcontrol accepts any non-zero exit, a timeout included. In this capture every expected row is present (60 rows inrun.txt: 12 rpc-client, 42 rpc-contract, 5 controls, 1 test), everypaths/section is present (70 inrenderers.txt, 12 incontracts.txt, 42 inlive-worker.txt), no cell isno-config, and all 12bogusrows exit 1 through both implementations. - One set of generation inputs. One cluster name, endpoint, installer image and secrets bundle, no patches at generation, no docs or examples. Other options may change which contracts differ.
- The policy points are a sample. Kubernetes 1.29.0 to 1.37.0 (ten points) and Talos v1.10 to v1.15 (six points). Bounds outside that range are not measured.
- Upstream documentation is dated by reading, not by the page. Neither support-matrix page carries a last-updated date, and the two disagree (§4.4).
- The
--mode rebootobservation (§6.2) comes from each binary's--help, read after the capture; it is not in the committed evidence. Nor are thetalosctlbinary sizes (§6.2) or the 404 of the older support-matrix URL (§4.4). E3_OUT's guard is a prefix test on the path as given. It refuses any path beginning with the checkout's path, a sibling such as<checkout>-outincluded, which fails closed. Nothing is canonicalized, so a path that reaches the checkout through a..component or a symlink without beginning with its path passes.- A missing pin aborts with the wrong message.
e3_sha(run/lib.sh) dies whenversions.envpins no digest for a version, but it runs inside a command substitution, so only the subshell exits. The caller goes on with an empty digest, which still fails closed:fetchreports a sha256 mismatch andtcreports that the binary is not the pinned one. Every version run here is pinned. - Kubernetes component images inside the nodes are pinned by version, not by digest (fixtures report).
8. Recommendation¶
For the PoC's existing-cluster configuration control:
- Pin the renderer to the node's minor, as the upstream client guidance recommends. In this
matrix, a v1.13 renderer's default output is accepted by the v1.13.6 node and a v1.14 renderer's is
refused. Only
talosctlwarns about the skew, and only on reads. - Refuse, in Bronzeward itself, any target contract whose minor exceeds the renderer's, and any Kubernetes version outside the target's window. The renderers refuse neither (§6.3 items 1 and 5). §6.5's "prerequisite" is therefore Bronzeward's check to make, not the renderer's.
- Record the contract, not only the renderer. A release that records "rendered by v1.14.1" does not say whether a v1.13 node can decode it; the contract does.
- Choose between subprocess and machinery on packaging, not compatibility. Within this matrix they reached the same outcomes wherever both were measured (§6.2). Either way, several renderer versions mean several binaries.
- Treat the machinery's
compatibilityanswers as permissive, wider than the documented tested path. A Bronzeward support policy, if one is wanted, is a separate decision.
9. Hand-off¶
- To renderer selection: §6.2's packaging table, the silent clamping (§4.1), and the contract-versioned paths under v1.14 (§6.2). Either implementation returns the configuration diff as text in its errors and dry-run results; the filter that keeps it out of logs and evidence should fail closed, unlike this prototype's (§7).
- To the feasibility review: the deferred lifecycle execution (§6.3) is a permanent caveat of this investigation; design E3 did not pass (§6.4).
- To issue 3's follow-ups: composition parity
through the machinery's
configpatcher, and on renderers other than v1.13.6, is inferred here, not measured (§6.2).