21. Pinned Kernel Artifact Contract with cordanaLLM/nucleus¶
Date: 2026-09-15
Status¶
Accepted. Amended by ADR-0028; decision 4 superseded by ADR-0030.
Context¶
cordanaLLM/nucleus is the kernel forge that produces the kernel artifacts this image forge consumes (planning step step-nucleus-contract, requirement req-nucleus-contract). Until this decision the two repositories were coupled by loose repository_dispatch payloads:
sync-kernel-manifest.ymlwrotekernel.streams.<stream>.versionintoversions.jsonstraight from thekernel_release_publishedpayload, fell back to a default stream and version when the payload was incomplete, verified nothing, andversions.schema.jsonhad nokernelproperty, so the write was outside the Single Source of Truth contract (ADR-0001).dispatch-kernel-requirements.ymlsent nucleus a fixed triple (drivers,runtimes,kubernetes.version) that nucleus does not read; itsverify-requirements.ymlchecks a hard-coded symbol list.
Two upstream facts fix the shape of the replacement. nucleus publishes GitHub Releases with *.deb, a UKI .efi, CycloneDX and SPDX SBOMs, SHA256SUMS, and a keyless cosign bundle SHA256SUMS.bundle (cosign sign-blob --bundle), signed by its publish-release.yml workflow identity. cordanaLLM/Aegis-OS defines the kernel requirement payload aegis.p01-nucleus.kernel-requirement.v1 (build/kernel-requirement.json at 5148ab27bb4c9d32759e8739b97c05d9b423b912): a correlation id, architectures, an ABI floor, and a feature list where each CONFIG_* symbol carries a required state, a probe method, and the requirement it serves. No nucleus release exists yet, so no real digest can be pinned today.
The planning acceptance criteria are explicit: a pinned artifact with a valid digest is consumed; a tampered artifact digest is rejected; an empty kernel requirement list is rejected explicitly.
Decision¶
- Artifact contract owned by the consumer.
pkg/kerneldefinesimago.nucleus.kernel-artifact.v1:provider,stream,version,kernel.release,kernel.config_digest(sha256:over the shippedkernel-<stream>.config), 1..64artifactswith safe basenames, 64-hexsha256, and boundedsize,checksums(SHA256SUMSand its digest), andprovenance(repository,tag, 40-hexrevision,bundle,signer_identityunderhttps://github.com/<provider>/). Decoding is strict: unknown fields, trailing data, and documents above 1 MiB are rejected; every error is correlated by<stream>@<version>and field.nucleusgenerateskernel-<stream>.manifest.jsonin this shape inpublish-release.yml, uploads it with the other assets, and extends its dispatch payload to{"stream", "version", "tag"}. - Division of verification.
cosign verify-blobinsync-kernel-manifest.ymlis the only signature check: it verifiesSHA256SUMS.bundleagainst the certificate identity prefix derived fromkernel.providerinversions.jsonand the GitHub Actions OIDC issuer. The Go verifier (kernel.Verify, exposed asimago kernel artifact verify) checks what cosign does not: theSHA256SUMSdigest, that every listed artifact appears inSHA256SUMSwith the same digest, is a regular file of the declared size, and hashes to the declared SHA-256 under a 512 MiB bounded read; that the bundle file and signer fields are present; and that the manifest matches the payload (--expect-stream,--expect-version,--expect-tag) and the provider and contract pinned inversions.json. Failures are typed sentinels (ErrDigestMismatch,ErrSizeMismatch,ErrMissingArtifact,ErrChecksumEntry,ErrMissingBundle); an unlisted extra file is reported, not rejected. Cosign verification is not reimplemented in Go. - Pin before consumption.
versions.jsongains a typedkernelsection (provider,contract,dispatch_event,streams), validated bypkg/manifestandversions.schema.json; each stream pinsversion,artifact_digest(thesha256:digest ofSHA256SUMS), andprovenance.tag/revision/bundle. The workflow writes a stream only after steps 1 and 2 pass, validates the schema, and proposes a pull request that adds onlyversions.json. A payload withoutstream,version, andtagis refused; the default-version fallback is deleted.streamsis committed empty because no release exists to pin. - Aegis-shaped requirement.
imagokeeps its own requirement inkernel/requirement.jsonin the Aegis shape (correlation idimago-kernel-requirement-0001,required-byids naming flavor tiers) and vendors the Aegis fixtures underpkg/kernel/testdata/. The decoder enforces the exact schema id, a 1..128-character correlation id, 1..8 unique architectures, a dotted numericabi.minimum-release, 1..512 features with uniqueCONFIG_[A-Z0-9_]+symbols,stateinbuilt-in/module, a probe from the package-level allow-list (kernel-config,lsm-list,btf-vmlinux,powercap,iommu-groups; extension is a one-line change), and a boundedrequired-by. An empty feature list is rejected withErrEmptyRequirement.dispatch-kernel-requirements.ymlvalidates the file and dispatches its path,sha256, and commit rather than the file's content or a fixed symbol list.
Consequences¶
- Positive: a kernel artifact reaches an image build only through a stream pinned in
versions.jsonafter signature, checksum, digest, size, and provenance verification; the bounds (1 MiB documents, 64 artifacts, 512 MiB per artifact, 1024 checksum lines, 4096 directory entries) keep the verifier within HISS-02/03. - Positive: the requirement imago sends upstream is the same document shape Aegis-OS sends, so nucleus needs one consumer for both, and the probe vocabulary is shared.
- Negative: the dispatch payload shape is a breaking change; a nucleus release published without
kernel-<stream>.manifest.jsonor withouttagin its payload is refused bysync-kernel-manifest.ymluntil the nucleus-side change is merged. - Negative:
kernel.streamsstays empty until the first nucleus release passes verification; consumers ofversions.jsoncannot assume a pinned kernel exists. - Neutral: nucleus's
verify-requirements.ymlstill evaluates its fixed symbol list; consuming the Aegis payload shape on the nucleus side is tracked there and does not weaken the downstream verification decided here.