feat(nix-setup)!: default-on, fail-closed pooled Nix cache read edge (TIN-4299) - #166
Draft
Jess Sullivan (Jesssullivan) wants to merge 2 commits into
Draft
feat(nix-setup)!: default-on, fail-closed pooled Nix cache read edge (TIN-4299)#166Jess Sullivan (Jesssullivan) wants to merge 2 commits into
Jess Sullivan (Jesssullivan) wants to merge 2 commits into
Conversation
…(TIN-4299) BREAKING CHANGE: attic-public-read defaults to "true" on nix-setup, nix-build, and greedy-cache; lands in the next MAJOR cut. TIN-4299 ruling 4 (2026-09-04, operator-approved): a consumer that never declared trust must not silently build cache-less or substitute from an unverified cache. The opt-in attic-public-read edge had exactly that fail-open shape: default "false" meant most lanes ran with no substituter at all, and even opted-in lanes treated DNS resolution as reachability, silently skipped a tenant server with no key, and never checked that the key named the cache being substituted. nix-setup default path now: resolve the endpoint (attic-server input > ATTIC_SERVER from the org overlay / fleet env > last-resort tinyland-inc public-read "main" cache), resolve the trusted key (input > env > baked key for the default server only; a non-default server with no key is a hard failure), require the key to be <cache>:<base64>, decode to a 32-byte ed25519 key, and name the substituted cache, then GET <server>/<cache>/nix-cache-info and require "StoreDir: /nix/store". Any failure is ::error + exit 1. Only then is nix.conf written and ATTIC_PUBLIC_KEY exported; attic_public_key is always emitted on the default path and attic_reachable reports the probe. With ATTIC_TOKEN in scope the authenticated caller still owns nix.conf trust (::notice, never a silent skip) and the probe still runs with the token so a private cache answers. ATTIC_SERVER is never exported from the last-resort default and no push path is enabled. attic-public-read: "false" is the explicit opt-out for lanes that never touch Nix and restores the pre-TIN-4299 path byte-for-byte (offline harness: stdout, GITHUB_ENV, GITHUB_OUTPUT, curl log cmp-identical to origin/main for the three opt-out cases; no nix.conf on either). The baked nix-cache.tinyland.dev server + key stay only as the last-resort default (docker/dind runner types never receive the fleet env); that consumer-surface literal is retired by the typed NixCacheSupply (endpoint class + trust-root digest) in the TIN-4299 packet. nix-build and greedy-cache pass the value through explicitly, so their defaults flip too; leaving them at "false" would silently opt every caller out. Their token-absent warning gates are unchanged. AGENTS.md rule 2 gains its third standing exception (precedent TIN-3914); CHANGELOG [Unreleased] carries the entry under "Changed -- BREAKING (next MAJOR)". Callers pinned to @V3 / @v2.12.1 / v5.x are unaffected until they move pins. Receipts: nix develop --command just check passed (lint-runs-on 27 checked 0 FAIL; no-hosted-runners 0 hosted labels across 38 files); offline harness 14/14 cases (absent key, unreachable, not-a-cache, key/cache mismatch, bad key length, unshaped key, DNS unresolvable, token-in-scope probe pass/fail, tenant happy, default happy, three opt-out cases).
Jess Sullivan (Jesssullivan)
force-pushed
the
feat/tin-4299-attic-public-read-fail-closed
branch
from
September 5, 2026 01:41
b7d0a8f to
acfa448
Compare
… TIN-3836 R3 (TIN-4299) Root cause: TIN-4246 steamroll audit (comment c2093793, 2026-09-05) found that #166 flipped `attic-public-read` to default-on in `nix-build` and `greedy-cache` while both pass-throughs still called floating `tinyland-inc/ci-templates/.github/actions/nix-setup@v3`. `uses:` resolves nix-setup at ITS OWN ref, so the flipped default reached the v3-line opt-in nix-setup (warn-and-degrade), not the fail-closed one this PR ships: default-ON but fail-OPEN. The same audit found the PR restored the operator's 2026-08-17 TIN-3836 ruling 3 design without citing it. Amend, in place on the extant carrier: - `nix-build` / `greedy-cache`: pin `nix-setup@v6.0.0`, the MAJOR the `[Unreleased]` BREAKING entry lands in. The repo's composite-action-pin contract (scripts/validate-ci-templates.py check_internal_refs; RELEASING.md "Composite-action internal refs") admits only an exact `vX.Y.Z` self-release or the current line `@v3`: a commit-SHA ref fails `internal-refs-check` (measured), and a consumer-relative `./` ref is named as not release-vendored (RELEASING.md; rust-bazel closure check). The release commit retargets both pins if it names another version (RELEASING.md step 3). - CHANGELOG `[Unreleased]` entry + AGENTS.md rule-2 exception: cite TIN-3836 ruling 3 verbatim ("ci-templates reader: GF-parity degrade — public substituter + ATTIC_PUBLIC_KEY unconditional; token gates netrc/push only; loud warning on degrade"), note #134 shipped it as the opt-in default, and record the pass-through pin. Carrier: TIN-4246/comment/c2093793 (steamroll audit, amend-class).
Jess Sullivan (Jesssullivan)
marked this pull request as draft
September 5, 2026 21:55
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Carrier: TIN-4246/comment/c2093793 (steamroll audit, amend-class)
Amendment 2026-09-05 — steamroll audit c2093793 (amend-class, in place)
The TIN-4246 steamroll audit (Linear comment
c2093793-8ddf-4531-8c40-0b224fbe79be, 2026-09-05T18:36Z) found two defects in this PR as opened. Both are corrected on this carrier in commit25c0ffa; no parallel carrier.nix-buildandgreedy-cachepassed the flippedattic-public-read: "true"through to floatingtinyland-inc/ci-templates/.github/actions/nix-setup@v3.uses:resolves the nested composite at its own ref, so the pass-throughs reached the v3-line opt-innix-setup(feat(attic): opt-in tokenless read degrade (TIN-3836) #134 warn-and-degrade), never the fail-closed one this PR ships. Fix: both pass-throughs self-pinnix-setup@v6.0.0, the MAJOR the[Unreleased]BREAKING entry lands in. That is the only pin form the repo's own contract admits and resolves to the fail-closed action:scripts/validate-ci-templates.py check_internal_refs(just internal-refs-check) accepts an exactvX.Y.Zor the current line@v3; a 40-char commit SHA fails it — measured 2026-09-05T21:54:08Z, rc=1 fornix-setup@acfa448f…, rc=0 for@v6.0.0../.github/actions/nix-setupis named inadmissible by RELEASING.md "Composite-action internal refs" (:112-114) and rejected as "not release-vendored" byvalidate-ci-templates.py:854-856; at runtime it would resolve against the consumer's workspace, not this repo."false"default. The CHANGELOG entry and the AGENTS.md rule-2 exception now cite it.Hold: converted to Draft. ci-templates merges at 0 approvals and cuts tags from
origin/main, so "opened unarmed" was no hold here; Draft is the only hold this repo has. Not armed.Receipts (amendment commit
25c0ffaf2244c9d242653b66a016f91eb0244f6f, GPG-signed, verifiedGood signature):Ruling executed
TIN-4299 ruling 4 / lane F2-ii (operator-approved 2026-09-04): the pooled Nix cache consumer edge becomes default-on and fail-closed. BREAKING — lands in the next MAJOR cut, not in a v5.x patch. Opened unarmed for that reason.
Rule-2 exception (AGENTS.md)
Rule 2 forbids a default flip unless it is a standing exception with a MAJOR bump (precedent TIN-3914). This PR adds the third standing exception to rule 2: a consumer that never declared trust must not silently build cache-less or substitute from an unverified cache, so the default flips and the edge hard-fails;
attic-public-read: "false"stays the explicit, byte-identical opt-out for lanes that never touch Nix.Behavior (
.github/actions/nix-setup/action.yml)Default path (
attic-public-read: "true"), in order:attic-serverinput >ATTIC_SERVER(org overlay / fleet env) > last-resort tinyland-inc public-readmaincache. An explicit/auto-detected server whose DNS does not resolve is now a hard failure (was warning + cache-less).attic-public-keyinput >ATTIC_PUBLIC_KEYenv > (default server only) baked key. Non-default server with no key = hard failure (was silent "no substituter"). Key must be<cache>:<base64>, decode to 32 bytes (ed25519), and name the substituted cache; else hard failure.GET <server>/<cache>/nix-cache-info(10s) must succeed and containStoreDir: /nix/store. DNS alone is not reachability.attic_reachablereports the probe.extra-substituters/extra-trusted-public-keysto~/.config/nix/nix.conf, exportATTIC_PUBLIC_KEY, emitattic_public_key(always emitted on the default path).ATTIC_TOKENin scope: the authenticated caller owns nix.conf trust (::notice, never a silent skip); the probe still runs — with the token, so a private cache answers — and still fails closed.Unchanged:
ATTIC_SERVERis never exported from the last-resort default; no push path is enabled here;nix-build/greedy-cachetoken-absent warning gates work as before.nix-buildandgreedy-cachepass the value through explicitly, so theirattic-public-readdefaults flip to"true"too (leaving them at"false"would silently opt every caller out).Decision D3 — baked literal
https://nix-cache.tinyland.dev+main:eaUy…are kept only as the last-resort default (docker/dind runner types never receive the fleet env). The literal is a consumer surface naming provider placement; its retirement is the TIN-4299 packet's ADD-TYPE item (NixCacheSupply: endpoint CLASS + trust-root digest). Stated in the action comment, README, and CHANGELOG.Pin insulation
spoke-ci.ymlandjs-bazel-package.ymlreferencenix-setup@v3;nix-buildandgreedy-cacheself-pinnix-setup@v6.0.0(amended 2026-09-05, see top — floating@v3there was default-on but fail-open);spoke-ci-restricted.ymlpins@v2.12.1;spoke-ci-v4.ymlcalls no Nix action at all. No consumer changes behavior until it moves pins to the MAJOR that carries this. The release cut should adddocs/migration-v5-to-v6.md(not in this PR).Receipts
Offline harness (extracts the
detectstep, stubsgetent/curl, fakeHOME/GITHUB_ENV/GITHUB_OUTPUT, operator env scrubbed):Byte-identical opt-out proof: for the three
optout-*cases,stdout,GITHUB_ENV,GITHUB_OUTPUT, and the curl log arecmp-identical between origin/main'snix-setup/action.ymland this branch's, with nonix.confwritten by either.Files:
.github/actions/nix-setup/action.yml,.github/actions/nix-build/action.yml,.github/actions/greedy-cache/action.yml,README.md(§ Attic tokenless read),AGENTS.md(rule 2, third exception; TIN-3836 R3 citation),CHANGELOG.md([Unreleased]→### Changed — BREAKING (next MAJOR); TIN-3836 R3 citation + pass-through pin).