Repository navigation
Description
Platform CI gets a mandatory job images. It renders the tree as Flux applies it, every Kustomization with the locked flux2, refuses every HelmRelease for good (#49 line 2; a chart is rendered by a shipped command into plain manifests in the repository, which this job judges like any object), and refuses every image of every rendered Pod template unless it is an attested release of an overnightworks app or a digest-pinned image of an allowlisted vendor repository. It is a boundary over every rendered object and over the render inputs CI controls (#50 expectation line 2: "B1 stays a boundary in full"; ruling C, not option A's tripwire). What it cannot render is named under Named gaps; the admission check in the cluster (#50, not built yet) is to be a second layer after the merge.
Caller
- platform#4 (P1), its Ruled guard, and the live-go plan's slice S6 (D5 checks 1 to 3 of P1: every app release yields an attested private image in GHCR #4 (comment), cells E1 to E4 and E7 to E9, no Dependabot cells).
- The first app ref (hopin's manifests, hopin runs on the platform from its attested private image, routed by Traefik #46) lands only after this job, so no app image reaches main unjudged (live-go plan, section 5: no ref by tag alone, no first ref before the guard).
- The first vendor refs: Traefik (Traefik runs in the cluster from manifests rendered from its pinned chart and answers 404 for every host without a route #44) and cloudflared (The platform's own Cloudflare tunnel runs cloudflared in k3s in front of Traefik #45), both planned now.
- The releases it judges: hopin#537 (first), presentator#851; songmaker and speech-runtime are pending. quality#406 and quality#407 are closed (quality PRs presentator's web holds its secrets and its service-account token in one volume #409 and presentator's web holds its secrets and its service-account token in one volume #408, landed at 7715afe); hopin#537 is open and hopin has no release workflow yet.
- The admission item (draft, the check in the cluster): reads this job's vendor list and is a second layer after the merge, which also judges what this job cannot read, by form and attestation. It is no boundary for this job's tag and signer checks (point 9).
Ruled
- Rendered output (Platform CI refuses every image reference that is neither an attested app release nor a listed vendor at a digest #49 line 1, ruled C, the operator in chat: „ja wenn C besser ist bauen wir C!“): B1 judges rendered output. Each Flux Kustomization path is built with flux build or kustomize, each HelmRelease is refused for good (line 2 below), and the image rule applies to the rendered objects only. postBuild substitutions from Secrets stay a named gap, and B1a's source-parsing script is replaced.
- HelmReleases refused for good (Platform CI refuses every image reference that is neither an attested app release nor a listed vendor at a digest #49 line 2, ruled A, the operator in chat, 07.11.2026: „ja ?> wenn es besser ist und es profis so machen würden stimme ich zu! wir wollten ja nur bauen was wir müssen und auch kein theater machen! also es soll klappen logisch sein und dem standard folgenß“): the mandatory job
imagesrefuses every HelmRelease, and every chart is rendered by a shipped command into plain manifests committed to the repository under a regeneration gate (the rendered-manifests pattern; Traefik runs in the cluster from manifests rendered from its pinned chart and answers 404 for every host without a route #44 is the first, withmise run charts:render). B1c (B1c: the job images renders each HelmRelease with helm template and judges its images #60) is closed as superseded; a later chart that needs a HelmRelease reopens it. helm-controller leaves Flux in its own item, "Flux runs without a Helm reconciler" (Flux runs without a Helm reconciler #71). - The guard (P1: every app release yields an attested private image in GHCR #4, Ruled; recorded there from quality's head, 06.11.2026, with no operator words quoted): platform's CI refuses a bump whose app tag is missing or points at another commit than the attested image; quality's image workflow builds no guard of its own.
- The bump path (the operator, 06.11.2026, platform's head tab, recorded on Platform on k3s and Flux: from an empty PC to every app live, with hal archived #1 and P1: every app release yields an attested private image in GHCR #4): the release's last job opens the bump pull request through an organisation GitHub App; he merges, then Flux deploys; Dependabot's docker ecosystem is not used for app images. His words: „so dass ich sofort updaten kann wenn ich will?“ Bearing here: no Dependabot entry and no Dependabot cell; this job is what makes such a pull request mergeable or not, since main requires
quality / verdict. - Only a
v*tag goes live (hal ruling of 16.10.2026, as P1: every app release yields an attested private image in GHCR #4 records it). - Platform on k3s and Flux: from an empty PC to every app live, with hal archived #1, Invariants 2 and 6: nothing a maintained tool does is built here, and every tool piece names the tools it was checked against; the fleet's quality gate holds from the first pull request.
- Global code policy: an allowlist is a protective limit; values that vary by vendor have one owner.
Decided by the head
Re-planned to line 1's ruling C. Points 1, 2, 7, 9 and 10 change; line 2's ruling A (07.11.2026) removes the HelmRelease render from them. Points 3 to 6 and 8 stand. The rounds of 06.11.2026 judged the replaced source-parsing design.
-
The job renders, it does not decode.
flux build kustomization <root> --dry-run --in-memory-build --recursiveof the locked flux2 builds the root Kustomization fromclusters/pc/flux-system/gotk-sync.yaml, in a container without network, so a remote kustomize resource fails the render. Every nested Kustomization on this repository's GitRepository is built with Flux's own generator,images,patchesand inlinepostBuild.substitute; a suspended one too.- The render is trusted only as far as it is the tree Flux applies. Red: a rendered root that differs from the committed one; two Kustomizations with one identity; a health-checked controller the render lacks; a mapped source with
include,ignore,recurseSubmodulesorsparseCheckout; a root GitRepository other than this repository's default branch, read from the run; a platform-secrets url other than the owner's repository named as the object (an optional.git; this repository and every other repository of the owner red); any.sourceignore;ignoreMissingComponents: true. - A HelmRelease is not rendered: it is red for good (line 2). A chart reaches the tree as committed plain manifests that its generator writes under a regeneration gate (Traefik runs in the cluster from manifests rendered from its pinned chart and answers 404 for every host without a route #44's
charts:render), and the rule judges them like any other object. - The rule judges every container, init container, ephemeral container and image volume image of every Pod template in Pod, PodTemplate, ReplicationController, ReplicaSet, Deployment, StatefulSet, DaemonSet, Job and CronJob, matched by apiVersion and kind.
- Another kind that holds an
imagekey or a host-qualified ref with a tag or digest is red. Exempt, by group, version and kind: akustomize.toolkit.fluxcd.io/v1Kustomization the build built, and anapiextensions.k8s.io/v1CustomResourceDefinition. Everyhelm.toolkit.fluxcd.ioobject is red for good. - Case: the registry host and the repository path must be lowercase; a tag follows
[\w][\w.-]{0,127}. - A render that fails is red with the renderer's message, and a source that is not mapped fails the render.
- The PC-only source
platform-secretsis mapped to an empty source and named in the log. - A
substituteFromSecret renders its variables empty, at their defaults or literal, and is named in the log; asubstituteFromConfigMap is red. - In flux-system, an image that
flux install --exportof the locked flux2 names passes as Flux's own.
The old point 1's constructs (escapes, aliases, custom tags, map keys, YAML in strings, patch operations, split mappings,
imagesoverrides, replacements and their delimiters, fieldPaths, case-insensitive kustomize keys, ImageTagTransformer, generator literals, repeated keys) are now resolved by kustomize or the chart. The rule judges only their rendered result; a repeated key in a manifest fails the render, and one inside a patch string is not driven (U-7 of the re-plan). The exempt generated file and the two policy-file exemptions go.Weighed:
- The Kustomization render, chosen:
flux build kustomization --dry-run --in-memory-build --recursive --local-sources. It is Flux's own build code at the locked version, one command renders the whole tree, an unmapped source fails loud with no code of ours, and in a container without network it fetches nothing. - Refusing remote kustomize resources, chosen: the render runs with
--network none, which refuseshttps://files and git bases alike at the pull request, in B1's one script. Rejected: kustomize-controller--no-remote-bases=trueas the fix (it refuses only kustomize's git clones, nothttps://file resources, lets CI pass what the PC then refuses, and needs a scope addition and aclustercell; kept as Only attested app images and digest-pinned listed vendor images start on the PC #50's hardening follow-up); a dead proxy plusGIT_ALLOW_PROTOCOL(several unverified bypass paths);sudo unshare --net(needs root and differs from local runs); detecting remote entries inkustomization.yaml(source parsing again, against the ruling). kustomize buildorkubectl kustomizeper path, rejected:./clusters/pchas nokustomization.yaml(Flux generates it), and a Kustomization'simages,patches,postBuildandtargetNamespacelive outside kustomize, so the script would reimplement Flux.- flux build without
--recursive, rejected: code for what the flag does. - A changed self-reference, chosen red by comparing the rendered root's
specwith the committed one. Processing it instead is a fixpoint loop for a state no tree needs, and the PC would run each intermediate spec in turn. --strict-substitute, rejected: it fails the whole render on any Secret variable, including ones outside images.- Reading the k3d cluster after
clusterreconciles, rejected: it tiesimagesto the 30-minute job and its success, misses hook Jobs already deleted, and the ruling names a build with flux build or kustomize and helm template in CI, not a reconcile. - flate (home-operations/flate), not chosen today: its Kustomization half is its own orchestration around kustomize's library, not Flux's code, with its fidelity resting on a five-month-old v0.x (not driven); it fetches every source with machine credentials and skips an unauthenticated one only under a broad
--allow-missing-kinds Secret; itsget imagescoverage of init containers, ephemeral containers and image volumes is unverified. It is AGPL-3.0 and the maintained successor of flux-local. Not re-weighed: no HelmRelease render is built (line 2). - flux-local, rejected: its README says it is deprecated and points to flate.
- HelmReleases, chosen (line 2): refused, and charts rendered into committed manifests by a shipped command. Rejected: rendering each HelmRelease in this job with
helm template(B1c; its plan drew six blocking findings, each a way CI's render can differ from helm-controller's render on the PC), kustomize'shelmChartsinflator (needs helm anyway, plus its own chart cache) and flate. - The rule, chosen: a yq extraction over Pod-template kinds and a bash judge. Rejected: kyverno CLI applying AD1a's ValidatingAdmissionPolicy to the rendered objects (AD1a is not built, kyverno CLI is a new tool, its
paramRefsupport offline is unverified, and the vendor-list rules and B1b still need the script; a follow-up once AD1a lands); conftest or kubeconform (a second policy language for one rule, and kubeconform checks schemas, not images); judging every string of every rendered object (false reds and no gain, since the node pulls only from Pod specs and unknown kinds fail loud). - Flux's own controllers, chosen: compare with
flux install --exportof the locked flux2 (a relationship read from the owner, it judges a hand edit, it runs offline). Rejected: a literal list of the four repositories (restates the generated file) and exempting the file (impossible once the tree is rendered). - The PC-only source, chosen: one named empty mapping in the script, a pair with the cluster overlay whose drift fails loud. Rejected: deriving it from
secretRef(a heuristic that would turn every private source into a silent gap). One owner for both jobs (a marker on the source object) is a follow-up. - The evidence, chosen: committed fixture trees run by the job, so about 70 sub-cells become CI evidence on every run, B1b cannot regress B1a's cells, and every flux2 bump through
mise lockis covered. Rejected: driven scratch trees only, which are cheaper today but last one review.
-
App or vendor, by a vendor allowlist platform owns. A classified ref is either an app ref under
ghcr.io/<owner>/, checked by points 3 to 5, or a vendor ref. A vendor ref must be<listed repository>[:<tag>]@sha256:<64 hex>: its repository is an entry of the vendor list, a tag before the digest is allowed, so Traefik's…@sha256:and Kyverno's…:v<version>@sha256:both pass, and the admission item's form rule holds the same grammar. Anything else is red, naming the file, the path and the decoded value:docker.io/someone/hopin@sha256:…is red.- The list's one owner: the ConfigMap
image-vendorsin namespaceadmission, fileinfrastructure/admission/image-vendors.yaml, which the admission item's policy reads as its parameter. The job reads its.datawith yq. Whichever of B1a and the admission item's first slice lands first creates the file; until that slice applies it, it is a file only this job reads. There is no second list. - Entries: one key per vendor (a name, since ConfigMap keys hold no
/), its value<registry>/<repository>, never a version, with a comment line above it naming the manifest that runs it and the vendor's own page naming that repository. The job compares an entry literally, without Docker Hub'slibrary/normalisation, sodocker.io/traefikdoes not matchdocker.io/library/traefikand is red. - CI fixtures: an image that only a CI step runs in the job cluster is no rendered object, so B1 neither judges nor counts it, and it has no entry in the PC's list. B1 keeps no copy of the list for k3d; how k3d admits these images is AD1a's design (Only attested app images and digest-pinned listed vendor images start on the PC #50).
- Entry rules the job enforces: an entry with a tag or a digest is red, and so is an entry under
ghcr.io/<owner>/. An entry no rendered image uses is red, so the list holds only what runs, and an entry leaves in the change that removes its last ref (the admission item's removal of Kyverno keeps that order). - Owner: platform's head owns the file. An entry changes only when a vendor is added or removed, in a pull request the operator merges (Q8 of Traefik runs in the cluster from manifests rendered from its pinned chart and answers 404 for every host without a route #44). Its review checks the vendor page and that the repository is not an app of ours under another name.
- Seed: the entries the tree uses when the job lands. Expected:
docker.io/cloudflare/cloudflared(The platform's own Cloudflare tunnel runs cloudflared in k3s in front of Traefik #45, first) anddocker.io/library/traefik(Traefik runs in the cluster from manifests rendered from its pinned chart and answers 404 for every host without a route #44). Thecurlimages/curlandtraefik/whoamiCI fixtures leave the seed. hopin needs no vendor image: it is stateless and reaches LiteLLM and Keycloak over the network. Keycloak and LiteLLM join with their manifests. The head's direction said to seed Traefik and cloudflared; the orphan rule defers each entry to the change that adds its first ref, and the cut in point 10 makes that this job's own landing or Traefik runs in the cluster from manifests rendered from its pinned chart and answers 404 for every host without a route #44's and The platform's own Cloudflare tunnel runs cloudflared in k3s in front of Traefik #45's.
Weighed: - the namespace alone (the draft): an app image under another registry passes;
- a list of vendor versions: drifts with every bump;
- matching an entry's last component against the organisation's repository names: a heuristic with a token scope, where the review of a rare diff serves.
- The list's one owner: the ConfigMap
-
The tag (unchanged). The image tag is the version without
v; the git tag isv<version>(quality'sdocs/release.mdat 7715afe: an app releasesYYYY.MM.DD.N, taggedvYYYY.MM.DD.N, and the caller passes the sameversiontoimage.yml; hopin#537 follows it).- Lookup: the job runs
git ls-remote https://github.com/<owner>/<repo> refs/tags/v<version> 'refs/tags/v<version>^{}'and keeps only lines whose ref name is exactly one of the two (ls-remote matches patterns against the tail). It takes the peeled^{}commit when it exists, else the direct one, so a lightweight tag (release-tag) and an annotated tag (today's hand tags) both resolve to their commit. - Why the tag, not the release: the tag job runs after the image job (
needs: [gate, <image job>]in hopin#537), so an image can exist without a tag. A tag left without a release is completed by hand (docs/release.md, Refusals). The tag is therefore what the guard requires. - Weighed: refusing annotated tags (the kind of tag proves nothing; the attestation and the gate do); no peel (an annotated tag reads as its tag object and fails
--source-digestwith a misleading reason).
- Lookup: the job runs
-
The attestation, only through flags on unforgeable fields, read by digest. The job runs
gh attestation verify oci://ghcr.io/<owner>/<repo>[/<name>]@sha256:<hex> --repo <owner>/<repo> --signer-workflow <owner>/quality/.github/workflows/image.yml --source-digest <tag commit> --source-ref refs/heads/main --predicate-type https://slsa.dev/provenance/v1 --deny-self-hosted-runners --format json.- By digest: gh gets the ref without its registry tag. The version is proven through git, and gh cannot then fail on a missing registry tag instead of on the attestation. This is new against D5 point 2.
- The predicate type is passed although it is gh's default, so a changed default cannot loosen the check.
- Nothing from
statement: the job reads no field of it. - From the API: attestations come from GitHub's API, not
--bundle-from-oci, which gh's help says cannot detect a renamed repository.
-
The signer's version (unchanged). For every verified attestation,
.verificationResult.signature.certificate.buildSignerDigestmust be 40 hex characters, andgh api repos/<owner>/quality/compare/<digest>...main --jq .statusmust printaheadoridentical. An empty field, a 404,divergedorbehindis red, naming the commit. Weighed:--signer-digestwith one quality commit (platform would copy every app's pin, D5 point 3); agit fetchof quality andmerge-base --is-ancestor(a clone for one bit the compare API gives). -
Grants and registry login (unchanged). The job grants
contents: readandpackages: read, plusattestations: readonly if cell E14 shows the API lookup needs it.- Login: gh's
oci://path reads the registry with Docker's keychain (cli/clipkg/cmd/attestation/artifact/oci/client.goat v2.103.2). A step therefore runsdocker login ghcr.io --username "$GITHUB_ACTOR" --password-stdinwith the job's token fromenv, never in argv. - Package grant: each package grants platform Read under its Actions access (O8, the operator's).
- Weighed:
docker/login-action(a pinned dependency for one command); a PAT secret (a secret where the job token serves).
- Login: gh's
-
flux2, yq and gh from the lock. flux2 is locked already; yq and gh (B1b) enter
mise.tomlandmise.lock(aqua:mikefarah/yq,aqua:cli/cli); helm enters with Traefik runs in the cluster from manifests rendered from its pinned chart and answers 404 for every host without a route #44's generator, never in this job, and the job installs them withscripts/bootstrap_pc.sh --only toolsasclusterdoes. yq reads the rendered stream and the vendor list.- Why: the verifier and the renderer decide what goes live, and the runner image's copies change with the image. gh's
--signer-workflowmatching has changed between releases (the end-anchored SAN pattern inpkg/cmd/attestation/verify/policy.goat v2.103.2). - Coverage: the
bootstrapjob's every-executable check then covers each without an edit. - Weighed: the runner's binaries (no diff shows a verifier change); a download with its own sha256 (a second install path beside mise).
- Why: the verifier and the renderer decide what goes live, and the runner image's copies change with the image. gh's
-
A script in
scripts/, run by its own job.scripts/verify_image_release.shis called from a jobimages. It holds functions per check, reports every failing ref and exits 1 if any fails. It reads the vendor list from its one owner (point 2).- Why a script: a program with these checks reads and lints better as a file than as a run block.
- Weighed: a step in
checks(it would widen that job's grants and hide which check failed). - Gate:
imagesentersmandatory_jobsandquality.needsin the slice that adds it (Platform on k3s and Flux: from an empty PC to every app live, with hal archived #1 invariant 6).
-
Tools checked against (Platform on k3s and Flux: from an empty PC to every app live, with hal archived #1 invariant 2). None replaces the script, because none checks the git tag or that the signer commit is on quality's main:
- Admission in the cluster (the admission item, draft: a native ValidatingAdmissionPolicy for the form and Kyverno's ImageValidatingPolicy for the attestation): a second layer after the merge. It judges every Pod the API server admits outside k3s's and Flux's own parts, by form and, for app refs in Pods, by attestation, so chart defaults, values from Secrets and ConfigMaps,
postBuildsubstitution and parser differences cannot hide an image from it. It is no boundary for this job's checks: it acts only after the merge, while the ruled guard refuses the pull request, and it cannot check the git tag or the signer commit on quality's main, so an app ref this job cannot read gets neither. Kyverno's Sigstore bundle path was read in source, not driven. - slsa-verifier: built for known builders such as slsa-github-generator; quality's reusable workflow is none of them (not verified against its current release).
cosign verify-attestation: could replace the gh call, but gh is the documented verifier of GitHub attestations, which quality'sdocs/release.mdnames; cosign's reading of the registry-pushed GitHub bundle is not verified.- flate, flux-local, kubeconform, conftest, kyverno CLI and reading the k3d cluster: each weighed in point 1 with its reason. None replaces the script: it also checks the git tag and the signer commit on quality's main.
- Admission in the cluster (the admission item, draft: a native ValidatingAdmissionPolicy for the form and Kyverno's ImageValidatingPolicy for the attestation): a second layer after the merge. It judges every Pod the API server admits outside k3s's and Flux's own parts, by form and, for app refs in Pods, by attestation, so chart defaults, values from Secrets and ConfigMaps,
-
Cut into two pull requests. The whole is beyond the slice limit. It splits along the seams of the render (B1c, B1c: the job images renders each HelmRelease with helm template and judges its images #60, is closed as superseded by line 2):
- B1a (B1a: the mandatory job images renders the tree as Flux applies it and refuses every rendered image that is neither a listed vendor at a digest nor an app ref #55): the Kustomization render, the Pod-template rule, the vendor list's rules, the fail-loud edges, the job and its fixture suite. Every app ref is red outright ("no release check yet"). B1a lands before Traefik runs in the cluster from manifests rendered from its pinned chart and answers 404 for every host without a route #44 and The platform's own Cloudflare tunnel runs cloudflared in k3s in front of Traefik #45, which then add their own entries and are judged from their first ref.
- B1b (B1b: the job images checks each app ref's tag, attestation and signer commit #56) with hopin's first release: gh, the login, points 3 to 5, which replace the outright red for each distinct app ref of the rendered objects. It lands once E1, E3, E4, E7 to E9 and E15 are driven against that release, and hopin runs on the platform from its attested private image, routed by Traefik #46 lands after it.
What is known
image.ymlat 89069a1 (quality#402, PR The weekly backup Schedule and its PodConfig are a component of their own #404; unchanged at 7715afe):- Calls and outputs: one image per call; outputs
digestandref=ghcr.io/<owner>/<repository>[/<image>]:<version>@<digest>, lowercased. - Build: builds the gated commit for linux/amd64.
- Attestation: with push,
actions/attest(v4.2.2,push-to-registry: true,create-storage-record: false) writes SLSA provenance v1. BuildKit's provenance and SBOM sit in the image, not separately signed. - The docs: quality's
docs/release.mdgives the consumer command without--source-digest.
- Calls and outputs: one image per call; outputs
- The certificate field. The signer digest is Fulcio's Build Signer Digest, OID 1.3.6.1.4.1.57264.1.10 (
docs/oid-info.mdin sigstore/fulcio; for GitHub itsjob_workflow_sha).- In gh: sigstore-go v1.3.0, which gh 2.103.2 pins, names it
buildSignerDigestincertificate.Extensions. - Read once, on 06.11.2026 with gh 2.103.2 against cli/cli's own attested release:
.verificationResult.signature.certificateholdsbuildSignerDigest,buildSignerURI,sourceRepositoryDigest,sourceRepositoryRefandrunnerEnvironment.
- In gh: sigstore-go v1.3.0, which gh 2.103.2 pins, names it
- The compare API, driven 06.11.2026:
compare/<signer>...mainprintedaheadfor 89069a1 and 8f766e3, anddivergedfor PR The weekly backup Schedule and its PodConfig are a component of their own #404's head 239e51e, which the squash merge left off main. - Tags today, driven with
git ls-remote:refs/tags/v2026.11.06.1alone yields hopin's tag object 831fa65; only the^{}pattern yields its commit df39828.- hopin, presentator and speech-runtime tag annotated by hand today; songmaker has lightweight tags beside annotated ones. release-tag creates lightweight tags at the gated commit.
- Driven, 07.11.2026 (the locked or named binary in a throwaway
ubuntu:26.04container,--network noneexcept for helm's chart pull, the repository mounted read-only, no kubeconfig):- flux2 2.10.0: main renders offline only with
--in-memory-build(36 objects, four images, all Flux's own);--recursivefails loud on an unmapped GitRepository or OCIRepository and with platform-secrets mapped empty equals the plain root build. - In a nested Kustomization
spec.images,spec.patchesand inlinepostBuild.substituteapply;substituteFromis skipped (null, a default, or literal);--strict-substitutefails the render. - The escape is decoded, the alias expanded, the custom tag dropped; a repeated
imagekey fails the render; image volumes are rendered. targetNamespace,namePrefix,commonMetadataandcomponentstogether renderDeployment/tns/pre-webwith the component's initContainer.- A changed root self-reference renders the root at
./appsand never builds it; a repeated nested identity leaves the second path unbuilt; both exit 0. A suspended Kustomization is rendered. - With network, an
https://file resource is fetched and rendered; with--network none, it and a git base fail ("network is unreachable"), exit 1. flux install --exportruns offline and equals the committedgotk-components.yaml.- yq 4.54.1: an 18-line extraction of
object, field path and image, and the unknown-kind detection, on the renders above. - helm v4.3.1: Traefik 42.3.1 renders by digest
sha256:a702a4316735397ecd1c3ed5287cccf87cdbc69abb0451c0b2d371408473bc33; the chart joinsimage.digestintodocker.io/library/traefik@sha256:<digest>; a wrong digest fails.
- flux2 2.10.0: main renders offline only with
- Read:
go.modfiles: flux2 v2.10.0 usespkg/kustomizev1.44.0 and kustomize api v0.21.2; kustomize-controller v1.10.1 the samepkg/kustomizewith kustomize api v0.21.1; helm-controller v1.7.1 uses helm v4.3.1.- flux2
internal/build/build.go:Buildskips a rendered Kustomization by name and namespace only, maps sources by key, and has no suspend check; no remote-base flag incmd/flux. - fluxcd/pkg
kustomizev1.44.0:GenerateManifestloads.sourceignorebelow the built path only;ignoreMissingComponentsskips a missing component. - kustomize api v0.21.1
FileLoader.Loadfetches a remote file with anhttp.Client, outside kustomize-controller's--no-remote-basesfilesystem. - Kubernetes v1.37.2:
ImageVolumeis GA and locked on since 1.36, so the node pullsvolumes[].image.reference. - helm v4.3.1:
ValidateReferenceinstalls by digest only without a version;templatehas--kube-version,--api-versions,--is-upgrade,--skip-tests. helm-controller v1.7.1:GetReleaseNameandGetReleaseNamespace. - mise registry maps
helmtoaqua:helm/helm; quality v0.9.2ROOT_BASE_DIRECTORIESincludestests/.
- Flux's patch fields in the committed schema: a Kustomization's
spec.patches[].patchand a HelmRelease'spostRenderers[].kustomize.patches[].patchare strings ("an inline StrategicMerge patch or an inline JSON6902 patch"). - The reference grammar (distribution/reference
regexp.go, main): a tag is[\w][\w.-]{0,127}. - The tree today: E10 of B1a: the mandatory job images renders the tree as Flux applies it and refuses every rendered image that is neither a listed vendor at a digest nor an app ref #55's matrix renders it: the root
flux-system, the GitRepositoriesflux-systemandplatform-secretsand the Kustomizationplatform-secrets, four images, all Flux's own. - Where attestations live. The app repositories are public, so
actions/attestsigns with Sigstore's public-good instance and uploads to GitHub's attestations API (actions/attest README at 1e69f48). The image name and digest of a private package therefore appear in the public transparency log; the source is public anyway. - No attested image exists in the organisation today (inferred):
- No caller: no app repository has a workflow calling
image.yml; hopin's workflows areci.ymlandcode-scanning.yml. - No test image: quality's CI checks
image.ymlonly statically. - The registry: GHCR's anonymous token is denied for every app name, as for a private or absent package. The organisation's package listing needs
read:packages, which the head's token lacks: not verified.
- No caller: no app repository has a workflow calling
- Named gaps. The job's reach ends at what the locked tools render. No layer on the PC judges these gaps yet. The admission item (Only attested app images and digest-pinned listed vendor images start on the PC #50) owns the second layer: the form rule on every admitted Pod and workload once AD1a lands, and the attestation check once AD2 lands. No tag or signer check reaches a ref B1 cannot render.
-
G1, PC-only source.
platform-secretsis not rendered, because CI holds no key; whatever it applies is not judged. It stands under the cost the operator accepted with C, „Werte aus deinen geheimen Einstellungen sieht sie trotzdem nicht“, and only while its url is the owner's repository that carries the object's name (S14). It is named in every log. The cluster job's overlay swaps the same source, and a new PC-only source turns both jobs red. -
G2, Secret substitutions (ruled).
- A variable from a
substituteFromSecret renders empty or stays literal, so an image built from it is red. - With a default and an inline variable beside it, the default renders. CI judges that default while the PC runs the Secret's value.
- A variable from a
-
G5, render fidelity.
- The flux2 CLI builds with kustomize api v0.21.2, and kustomize-controller v1.10.1 with v0.21.1.
-
G6, images outside Pod templates. These are not judged:
- an image inside a longer string of a non-workload object;
- an unknown kind's image written without a host and outside an
imagekey; - an operator's default image for a custom resource that names none.
Once AD1a lands, admission judges the Pods they become (Only attested app images and digest-pinned listed vendor images start on the PC #50).
-
G7, CI fixtures. Images that only a CI step runs in the job cluster are no rendered object. They never run on the PC and have no entry in its vendor list. How k3d admits them is AD1a's design (Only attested app images and digest-pinned listed vendor images start on the PC #50).
-
G8, Flux's controllers. They run tag-only in flux-system and pass as the locked flux2's export. Pinning them by digest is the vendor-pin bump item's job (Every vendor pin in platform's manifests has a maintained bump path #47).
-
G10, chart signatures. Neither the render nor Flux verifies the chart's provenance. This is Traefik runs in the cluster from manifests rendered from its pinned chart and answers 404 for every host without a route #44's gap, unchanged.
-
Kept, unchanged: the false red for a ref-shaped non-image string (now only in unknown kinds); forks; the registry tag not compared with its digest; old signers; tag creation.
-
G12, P6 leaves B1. A rendered image is always one string, so B1 no longer enforces that an app ref is written as one string in source. P6 stays hopin runs on the platform from its attested private image, routed by Traefik #46's and the bump path's (P1: every app release yields an attested private image in GHCR #4) convention.
-
Refusals (not gaps: each is red until a caller and a new decision):
postBuild.substituteFromConfigMaps: the dry run cannot read them.- Every HelmRelease, for good (line 2): a chart is rendered into committed manifests by its generator, which Traefik runs in the cluster from manifests rendered from its pinned chart and answers 404 for every host without a route #44 builds.
- Remote kustomize resources,
https://files and git bases: the render has no network (S1). - Changed or repeated self-references, and a render without the root's health-checked controllers (S1).
- Source fields and ignore files that make Flux's artifact differ from the checkout, and mapped sources other than this repository's default branch and the owner's secrets repository (S14).
-
- Invariants kept. No secret in the repository, a log or a test; the job token reaches the registry only through stdin; every check fails closed; nothing touches the PC, a package or a tag.
- Rollback. Revert the commit; removing a mandatory job is a declared loosening of
quality/config.toml. Nothing persistent is created. - Deletes. Nothing in the tree. D5's Dependabot route and its spike are not built (P1: every app release yields an attested private image in GHCR #4, comment 6686280549).
- Size. Against main: B1a about 220 production lines added and 2 removed, in five files; B1b about 60 in three.
Slices
- B1a is B1a: the mandatory job images renders the tree as Flux applies it and refuses every rendered image that is neither a listed vendor at a digest nor an app ref #55 (S1 to S3, S5, S6, S8 to S10, S14, E2, E10 to E13, E16 to E30).
- B1b is B1b: the job images checks each app ref's tag, attestation and signer commit #56 (S4, S7, E1, E3, E4, E7 to E9, E14, E15).
Under ruling C, the slices' own matrices hold the cells. #55's fixture suite runs in the job (no hand-driven fallback), and #56's driven cells are named there.
Verification: CI on the pull request and the landing push; the driven cells; quality hygiene . printing hygiene: clean.
Unknowns
External facts, kept as unresolved evidence until a driven cell settles them with the locked tools (the Codex review's follow-up 4):
- The locked gh's output: whether it prints
buildSignerDigestat the path read with gh 2.103.2, and its--signer-workflowmatching. Settled by E1 and E8 with the versionmise.lockpins. - Attestations without the grant: whether platform's job token reads another public repository's attestations through the API without
attestations: read. Settled by E14; a user token did on 06.11.2026 (cli/cli). - Private packages: whether GHCR serves a private package of another repository to platform's job token once O8's Read grant exists. Settled by E1. Whether an API sets the grant: not verified.
- The kustomize skew between the flux2 CLI and kustomize-controller (G5).
- Dependabot runs: whether a Dependabot-triggered run in platform (its github-actions and opentofu entries) reads the private packages with
packages: read. If not, those pull requests turn red once the first app ref lands. Read at the first such pull request after hopin runs on the platform from its attested private image, routed by Traefik #46. - Issue states, read 06.11.2026: quality#406 and quality#407 closed; hopin#537 and presentator#851 open.
- First dispatch: which item owns hopin's first dispatch (O9): hopin#537 excludes it.
Neighbours
- P1: every app release yields an attested private image in GHCR #4, the parent; its bump-path slice (route A, the GitHub App, O6) follows and opens the pull requests this job judges.
- B1c (B1c: the job images renders each HelmRelease with helm template and judges its images #60) is closed as superseded by Traefik runs in the cluster from manifests rendered from its pinned chart and answers 404 for every host without a route #44's rendered manifests (line 2); "Flux runs without a Helm reconciler" (Flux runs without a Helm reconciler #71) removes helm-controller after Traefik runs in the cluster from manifests rendered from its pinned chart and answers 404 for every host without a route #44. Live-go Traefik runs in the cluster from manifests rendered from its pinned chart and answers 404 for every host without a route #44 (Traefik) and The platform's own Cloudflare tunnel runs cloudflared in k3s in front of Traefik #45 (cloudflared) add the first vendor entries. hopin runs on the platform from its attested private image, routed by Traefik #46 (hopin's manifests and the pull credential) lands after this item. P6 keeps every app ref in one-string form as hopin runs on the platform from its attested private image, routed by Traefik #46's convention; B1 no longer enforces it (G12).
- The admission item of point 9 (draft), the second layer after the merge for what this job cannot read; it owns the vendor ConfigMap's namespace and applies it, and its two policy files skip this job's content rule. The vendor-pin bump item Traefik runs in the cluster from manifests rendered from its pinned chart and answers 404 for every host without a route #44 proposes, owner of vendor and Flux image bumps.
- quality#402, quality#406 and quality#407 (landed); hopin#537, presentator#851; songmaker's and speech-runtime's callers once opened.
version = 1
now = "B1a (#55) landed as 0f4cdaf (PR #57) and B1b (#56) as f284a22 (PR #67, 07.11.2026), so the job images renders the tree, judges vendor refs and checks each app ref's tag, attestation and signer commit; B1c (#60) is closed as superseded by line 2's ruling (07.11.2026): HelmReleases are refused for good and charts are rendered into the repository, and helm-controller leaves Flux in #71 after #44."
next = "B1's follow-ups are #65; then the head judges whether this item can close."
done_when = "On pull requests and pushes to main, the mandatory job images renders the tree with the locked flux2, without network, trusting the render only where it is the tree Flux applies (the committed root unchanged, this repository's default branch, no source field or ignore file that changes Flux's artifact). It refuses every image of every rendered Pod template, image volumes included, that is neither a repository listed in the admission item's vendor ConfigMap, with an optional tag, at a sha256 digest, nor a full app ref whose v-tag resolves, peeled when annotated, to the commit an image.yml attestation from quality's main names for that app's repository. Every HelmRelease, a kind the rule does not know that carries an image, a remote kustomize resource and a substituteFrom ConfigMap are red; platform-secrets and substituteFrom Secrets are named gaps. It names every failing ref, uses only read grants and the locked tools, and the scenario matrix is reported, E1 green against hopin's first attested release and E15 green on an annotated tag."
size = "L"
[[expectation]]
text = "Die Image-Prüfung im Pull Request fängt ehrliche Fehler deiner Agenten; die harte Grenze ist die Prüfung im Server."
asked_on = 2026-11-06
ruling = "C"
ruled_on = 2026-11-06
card_hash = "sha256:79ba123769b367ed8dbce9eaa0a2ce5c1b5d620504c214c3c7079c7cef732993"
via = "chat"
question = "Soll die Image-Prüfung im Pull Request nur ehrliche Fehler fangen, während dein Server die harte Grenze bleibt?"
problem = "Jede Prüfrunde findet neue Tricks, die Prüfung zu umgehen; fertig wird sie so nie."
decision_kind = "data_risk"
area = "Plattform"
topic = "Image-Prüfung"
recommended = "A"
reason = "Drei Prüfrunden hintereinander fanden je neue Schreibweisen, mit denen ein absichtlich getarntes Image durchkäme (Berichte vom 06.11.2026). Der Server prüft dagegen jedes startende Programm selbst, egal wie es beschrieben ist. Ein getarntes eigenes Image müsste zudem erst in dein Paketlager gelangen, in das nur deine Bauabläufe schreiben."
[[expectation.option]]
name = "Hilfe, Grenze im Server"
effect = "Die Prüfung im Pull Request fängt Tippfehler und vergessene Versionen deiner Agenten. Absichtlich getarnte Images hält der Server auf: feste Version und bekannte Hersteller sofort, die Herkunft, sobald das Prüfwerkzeug fertig ist."
notice = "Die Image-Prüfung wird bald fertig, und hopin rückt näher."
cost = "Bis das Prüfwerkzeug fertig ist, könnte ein absichtlich getarntes eigenes Image ohne Herkunftsnachweis starten, wenn es in dein Paketlager gelangt und du den Pull Request zusammenführst."
undo = "reversible"
undo_how = "Später wieder zur harten Grenze machen; die gefundenen Schreibweisen stehen als Liste bereit."
contract = "B1 (#49, #55, #56) becomes a tripwire under the global contract's Reviews: an aid for cooperative agents whose contract is the cases #49 and #55 list, the round-1 extensions of the head's #55 comment of 06.11.2026 included; a gap outside them is a follow-up on #49 and makes no lane material risk. The boundary that stops a non-cooperative author is the admission layer (#50): AD1a's form rule on every admitted Pod and workload, AD2's attestation check once Kyverno 1.20.x GA ships, and the operator's merge. #49 names the found constructs as gaps: case-insensitive kustomize keys, replacement delimiters on sources and targets, decoded options strings, ImageTagTransformer, generator literals, repeated patch-operation keys."
[[expectation.option]]
name = "Harte Grenze, weiter suchen"
effect = "Die Prüfung im Pull Request bleibt selbst eine harte Grenze; jede gefundene Schreibweise wird geschlossen, bevor sie fertig ist."
same_as_today = true
notice = "Die Image-Prüfung und damit hopin warten, bis keine Prüfrunde mehr etwas findet."
cost = "Wie lange das dauert, ist offen; jede Runde kostet Stunden und macht die Prüfung größer."
undo = "reversible"
undo_how = "Später zur Hilfe herabstufen."
contract = "B1 stays a boundary in full (#49); every construct a review finds is closed before B1a lands."
[[expectation.option]]
name = "Neu bauen auf Ergebnis"
effect = "Die Prüfung liest nicht mehr die Beschreibungen, sondern das, was die Werkzeuge daraus erzeugen, und prüft nur dort die Images."
notice = "Die Image-Prüfung kommt später, wird aber kleiner und schwerer zu umgehen."
cost = "Ein neuer Plan und etwa ein Tag Arbeit; Werte aus deinen geheimen Einstellungen sieht sie trotzdem nicht."
undo = "reversible"
undo_how = "Zurück zur heutigen Prüfung."
contract = "B1 is re-planned to judge rendered output: each Flux Kustomization path built with flux build or kustomize, each HelmRelease rendered with helm template and its values in CI, and the image rule applied to the rendered objects only; postBuild substitutions from Secrets stay a named gap; B1a's source-parsing script is replaced."
[[expectation]]
text = "Charts kommen als fertige Dateien ins Repository; Helm-Beschreibungen bleiben dauerhaft verboten."
asked_on = 2026-11-07
ruling = "A"
ruled_on = 2026-11-07
card_hash = "sha256:0e4a0daefe714c7db47687ea99e204ea280f9b5a96ab3e7fa3bfae5184d4ccee"
via = "chat"
question = "Sollen Charts als fertige Dateien ins Repository kommen, statt dass der Server sie selbst zusammenbaut?"
problem = "Fertige Dateien kann die Image-Prüfung vollständig lesen; eine Helm-Beschreibung baut erst der Server zusammen."
decision_kind = "technical"
area = "Plattform"
topic = "Charts als Dateien"
recommended = "A"
reason = "Fertige Dateien im Repository sind ein gängiges Vorgehen bei Flux (Rendered-Manifests-Muster). Die Chart-Prüfung hatte sechs Prüfbefunde, jeder ein Weg, wie die Prüfung etwas anderes sieht als der Server. Traefik wird schon so geplant."
[[expectation.option]]
name = "Charts als fertige Dateien"
effect = "Jeder Chart wird von einem mitgelieferten Befehl in fertige Dateien umgewandelt und im Repository gespeichert. Die Image-Prüfung verbietet Helm-Beschreibungen dauerhaft und prüft die Dateien wie alles andere."
notice = "Du siehst im Pull Request genau, was der Server bekommt; eine Chart-Änderung zeigt sich als Dateiänderung."
cost = "Ein Chart mit Installationsschritten passt nicht hinein; Helms Rückrollen und Versionsverlauf entfallen, Git ersetzt sie."
undo = "reversible"
undo_how = "Die Helm-Prüfung kann später gebaut werden, falls ein Chart sie braucht."
contract = "The mandatory job images refuses every HelmRelease for good, and every chart is rendered by a shipped command into manifests committed to the repository under a regeneration gate. B1c (#60) closes as superseded, and helm-controller leaves Flux in its own item."
[[expectation.option]]
name = "Später Helm-Prüfung bauen"
effect = "Die Image-Prüfung bekommt später die Fähigkeit, Helm-Beschreibungen selbst zu prüfen, bevor die erste auftaucht."
notice = "Der erste Chart wartet auf die Helm-Prüfung."
cost = "Ein weiterer Bauplan mit offener Prüfung; die Prüfung kann vom Server abweichen, wie die sechs Befunde zeigten."
undo = "reversible"
undo_how = "Später auf fertige Dateien umstellen."
contract = "B1c (#60) is built before the first HelmRelease: helm template renders each HelmRelease in the job, as #49 point 1 describes."
Activity
FlexOr2 commentedon Nov 6, 2026
Fact for P8 (songmaker's head, 06.11.2026): songmaker's release path (main 4a1cc8393) pushes three images under a nested path:
ghcr.io/overnightworks/songmaker/<image>:<YYYY.MM.DD.N>, where<image>isweb,music-workerorscoring-worker. An acestep worker follows later.B1's app-ref form and the admission layer's form (#50) know only
ghcr.io/overnightworks/<repo>:<tag>@sha256:<digest>. Both must admit the nested form before songmaker's slice. Each attestor then names songmaker's repository for all of its images.FlexOr2 commentedon Nov 6, 2026
Journal, 06.11.2026 (fact from songmaker's head): songmaker's Release workflow (songmaker 9a3a8d59b, quality's workflows pinned at 31900fcd) pushes four images,
ghcr.io/overnightworks/songmaker/{web,music-worker,scoring-worker,acestep-worker}:<YYYY.MM.DD.N>; acestep-worker is new and builds with its submodule. No release is dispatched yet; the first is the operator's to start. The attestor prefix must coverghcr.io/overnightworks/songmaker/*, and platform's package Read grant covers all four packages.The images job renders the tree with the locked flux2 and judges rend…
The mandatory job images renders the tree with the locked flux2 and r…
FlexOr2 commentedon Nov 7, 2026
Ruling of 07.11.2026 on line 2 (HelmReleases refused for good; every chart rendered by a shipped command into committed manifests under a regeneration gate), the operator in chat, option A. The head had asked whether HelmReleases stay refused for good or B1c (#60) is built later, named the cost (charts with install hooks do not fit) and the undo (B1c can still be built if a chart needs it), and recommended yes. His words, verbatim: „ja ?> wenn es besser ist und es profis so machen würden stimme ich zu! wir wollten ja nur bauen was wir müssen und auch kein theater machen! also es soll klappen logisch sein und dem standard folgenß“. The head's answer before recording: rendered manifests in Git are an established GitOps pattern (the rendered manifests pattern), so his condition holds. Line 1 (ruled C) is unchanged; this is a new line, not a re-rule.
The job images checks each app ref's tag, attestation and signer comm…
FlexOr2 commentedon Nov 7, 2026
The head revised
done_when's last clause on 07.11.2026. It now reads "E1 green against hopin's first attested release and E15 green on an annotated tag".Why. hopin's first attested release, v2026.11.06.2, carries a lightweight tag. Its ref points straight at commit 127f26c, so no real hopin tag can exercise E15's peeling. #56 drove E15 on an annotated tag in a local bare clone, and E1 against the real release.
Done. Every sentence of
done_whenis proven:The follow-ups in #65 change nothing the job refuses today, so #65 moves under #11 and this item closes.
FlexOr2 commentedon Nov 7, 2026
B1a (#55, 0f4cdaf) and B1b (#56, f284a22) landed and every done_when sentence is proven (comment above). B1c (#60) is superseded by line 2. The follow-ups live in #65 under #11.
Flux runs without helm-controller: its components, checks and the Hel…
FlexOr2 commentedon Nov 7, 2026
A gap for this list, from #81's review on 07.11.2026 (PR #85 at 569352b, follow-up).
scripts/verify_image_release.shnow judges and counts a host-qualified image in everyenvmap ofci.yml: the workflow's, each job's and each step's. Several image sources stay outside it:envmaps it does not read:jobs.<id>.container.envandservices.<id>.env;jobs.<id>.container.image,services.<id>.imageanduses: docker://…refs.Today's
ci.ymlhas none of these, so nothing goes unjudged now. The first of them a workflow adds needs the selector extended in the same pull request.