Skip to content

build(packaging): pin and hash the bundled setuptools wheel, with one source of truth - #306

Merged
bioedca merged 3 commits into
mainfrom
agent/issue-218
Jul 30, 2026
Merged

build(packaging): pin and hash the bundled setuptools wheel, with one source of truth#306
bioedca merged 3 commits into
mainfrom
agent/issue-218

Conversation

@bioedca

@bioedca bioedca commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Third PR of the swarm pilot, and the one the plan chose as its stress case — #218 previously ran
17 review rounds. Claimed as agent/issue-218, generation 38511909794, fence held across both
authoritative writes.

The defect

python -m pip download --only-binary=:all: --no-deps -d packaging/staging "setuptools<81"

It floats. Three OS runners resolve independently, so one release can bundle different setuptools
builds across platforms — and a rebuild of the same tag can bundle a different one again, with
nothing written down to say which. Every other component of this installer is pin-and-held through a
committed lock; this was the one exception, and it looked deliberate while being merely unpinned.

The pin had four homes, not three

The issue names three (two workflows + setup_sidecar.py). The contract test found a fourth: each
build env carried its own setuptools<81 in create-args — and because tMAVEN is built with
--no-build-isolation, that bound decided what tMAVEN actually built against, independently of
what shipped.

It is gone. The same hashed wheel is installed into the build env, so the build-time setuptools and
the shipped setuptools are one artifact by construction rather than by two bounds agreeing.

What lands

packaging/setuptools-compatibility.txt — the single source. setuptools==80.9.0 plus its SHA-256,
verified against PyPI rather than trusted from the grooming note: 1201486 bytes, py3-none-any,
so one artifact serves win-64 / osx-64 / osx-arm64 / linux-64.

Both workflows now:

  • download with --require-hashes -r packaging/setuptools-compatibility.txt;
  • stage the exact filename from setup_sidecar.py --print-setuptools-wheel, never a
    setuptools-*.whl glob;
  • re-hash the staged bytes. pip download succeeds on a cache hit too, so a stale
    packaging/staging/ left by an earlier unpinned run would otherwise be staged and shipped with
    nothing having checked it.

setup_sidecar.py parses the file instead of restating it, and installs in two pip commands
— it has to: hash-checking mode is all-or-nothing and the tMAVEN spec is a git URL with no hash to
give. Order is load-bearing: setuptools lands first, or tMAVEN builds against the lock's 82.x, the
release that removed pkg_resources.

The contract test, and two narrowings that are the point

test_no_consumer_restates_the_version_or_the_bound took three attempts, and each failure was
informative rather than noise:

  1. matching the version number flagged the docstring explaining that setuptools deprecated
    pkg_resources by 80.9.0 — deprecation history a reader needs, not a pin a bump must find;
  2. matching setuptools<comparator> flagged the module's own parser and an f-string echoing the
    value it had just parsed;
  3. requiring a literal digit after the comparator admits setuptools=={version} and the regex,
    rejects setuptools<81 and setuptools==80.9.0 — and that is what surfaced the fourth home.

Acceptance criteria

  • One committed hash-locked requirements file is the sole source
  • Both workflows download with hash enforcement and stage the exact pinned filename
  • setup_sidecar.py consumes the same source, hashed install split from the git-sourced tMAVEN
  • A contract test proves every consumer uses the one source and the staged name is exact
  • The tagged commit is sufficient to determine what shipped; no network-only resolution record
  • Documented as a temporary sidecar-runtime compatibility exception, with the removal trigger
    stated: tMAVEN no longer imports pkg_resources
  • Rationale recorded for retaining the older runtime-only wheel — and explicitly not waived
    from dependency auditing
  • Packaging gates are not exercised by this PR. packaging.yml is manual/tag-triggered, so
    the changed download and staging steps do not run here. That is stated rather than implied; see
    below.

What is verified, and what is not

Verified: the digest against PyPI; the parser against five malformed requirement files; the two-command
split and its order; the absence of any restatement in all three consumers; mkdocs build --strict.

Not verified here: the workflow steps themselves. Neither packaging workflow runs on a pull
request, so --require-hashes, the exact-name staging and the re-hash step are reviewed as code and
not as a passing run. The lowest-risk way to close that is a manual packaging.yml dispatch on this
branch before the next release; it is not a merge blocker for main, but it is the residual and
should not be discovered later.

Risk is high: .github/workflows/**, a dependency pin, and a release path.

Closes: #218

… source of truth

`pip download "setuptools<81"` floated. Three OS runners resolved independently, so one release
could bundle different builds across platforms, and a rebuild of the same tag could bundle a
different one again - with nothing written down to say which. Every other component of this
installer is pin-and-held through a committed lock; this was the one exception, and it looked
deliberate while being merely unpinned.

`packaging/setuptools-compatibility.txt` is now the single source: `setuptools==80.9.0` with its
sha256, verified against PyPI (1201486 bytes, `py3-none-any`, so ONE artifact serves all four
platforms). Both workflows download it with `--require-hashes`, stage it by exact name rather than
globbing `setuptools-*.whl`, and re-hash the staged bytes - `pip download` succeeds on a cache hit
too, so a stale `packaging/staging/` from an earlier unpinned run would otherwise ship unnoticed.

The pin had FOUR homes, not three. Beyond the two download commands and this script, each build env
carried its own `setuptools<81` in `create-args` - and since tMAVEN is built with
`--no-build-isolation`, that bound decided what tMAVEN built against. It is gone: the same hashed
wheel is installed into the build env, so the build-time and shipped setuptools are one artifact by
construction. The contract test found it, after two narrowings that are themselves the point - the
first flagged the docstring's deprecation history, the second flagged the module's own parser.

The install is two pip commands because it has to be: hash-checking mode is all-or-nothing and the
tMAVEN spec is a git URL with no hash. Order is load-bearing - setuptools lands first, or tMAVEN
builds against the lock's 82.x, the release that removed `pkg_resources`.

Documented as a temporary runtime-only compatibility exception with its removal trigger stated
(tMAVEN stops importing `pkg_resources`), and explicitly not waived from dependency auditing.

Closes: #218
Copilot AI review requested due to automatic review settings July 30, 2026 10:08

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 64acec61-638e-439f-8f3b-2881a5bf7871

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

@bioedca

bioedca commented Jul 30, 2026

Copy link
Copy Markdown
Owner Author

Diff declared final at 4de6d53; all 17 checks are green. Review round 1 of 2.

Risk is high.github/workflows/**, a dependency pin, and a release path → both providers,
requested together as one round.

@codex review
@coderabbitai review

The thing I most want attacked is what this PR cannot verify. Neither packaging workflow runs on
a pull request, so --require-hashes, the exact-name staging, the re-hash step and the new build-env
install are reviewed as code and never as a passing run. If any of them is wrong, the first
evidence is a broken release. Specifically:

  1. pip download --require-hashes -r <file> with --no-deps. Hash-checking mode is
    all-or-nothing. If --no-deps is insufficient — if pip still resolves something transitive and
    demands a hash for it — the download fails at release time and never here.
  2. The build-env change is the riskiest edit in the diff. setuptools was removed from
    create-args and installed by pip instead, after the micromamba env is created and before
    pip wheel --no-build-isolation builds tMAVEN. If that ordering is wrong, or if the conda env's
    own setuptools shadows the pip-installed one, tMAVEN builds against the lock's 82.x — the release
    that removed pkg_resources, i.e. exactly the defect fix(packaging): the installed sidecar ships setuptools 82.0.1, so idealization breaks on an installed app #212 reported.
  3. --only-binary=:all: on an install into the build env. Correct for a pure-Python wheel, but
    say so if it can force a failure where the old conda-provided setuptools would have succeeded.
  4. The staged-name assertion. expected="$(python scripts/setup_sidecar.py --print-setuptools-wheel)"
    runs in the build env's shell. If that interpreter cannot import what the script needs, or if the
    path is wrong from the workflow's working directory, the step fails loudly — which is fine — but
    check it cannot fail quietly into an empty expected that then matches nothing.

Two smaller ones:

  1. The parser accepts exactly one pinned, hashed requirement and rejects five malformed forms. If
    a sixth form gets through — a --hash for a different algorithm, an environment marker, an
    inline comment — it would be accepted while meaning something else.
  2. The pin had four homes, not the three the issue names. If there is a fifth — a doc that
    states the version as fact, a comment a bump would have to find — the single-source claim in the
    PR body is wrong and I would rather correct it than ship it.

The digest was verified against PyPI rather than taken from the grooming note: 1201486 bytes,
py3-none-any. The PR body records what is verified and what is not.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== PR metadata and changed files =='
gh pr view 306 --repo bioedca/tether --json headRefOid,baseRefOid,files,title,url
gh pr diff 306 --repo bioedca/tether --name-only

echo '== Candidate files =='
fd -a -t f '^(packaging\.yml|.*\.ya?ml|setup_sidecar\.py|setuptools-compatibility\.txt|.*setuptools.*|.*sidecar.*)$' . \
  -E .git -E node_modules -E dist -E build | sort

echo '== Python source outlines =='
for f in $(fd -a -t f 'setup_sidecar\.py$' . -E .git); do
  echo "-- $f"
  ast-grep outline "$f" --items all || true
done

echo '== Workflow and lock-file excerpts =='
for f in $(fd -a -t f '.*\.ya?ml$' .github packaging -E .git 2>/dev/null; fd -a -t f 'setuptools-compatibility\.txt$' . -E .git); do
  echo "-- $f"
  rg -n -C 8 --glob '*.yml' --glob '*.yaml' --glob 'setuptools-compatibility.txt' \
    '(setuptools|require-hashes|only-binary|setup_sidecar|staging|pip download|pip install|pip wheel|create-args)' "$f" || true
done

echo '== Repository-wide candidate pin/bound references (excluding VCS metadata) =='
rg -n -i -C 2 \
  '(setuptools\s*(==|<=|>=|<|>|~=|!=)\s*[0-9]|\b80\.9\.0\b|\b81\b|pkg_resources|setuptools-[^[:space:]]*\.whl)' \
  -g '!.git/**' -g '!**/node_modules/**' -g '!**/dist/**' -g '!**/build/**' .

echo '== Relevant test outlines and assertions =='
fd -a -t f -i '(test|tests).*' . -E .git | while read -r f; do
  if rg -q '(setuptools-compatibility|setup_sidecar|print-setuptools-wheel|restates_the_version|require-hashes)' "$f"; then
    echo "-- $f"
    ast-grep outline "$f" --items all || true
    rg -n -C 12 '(setuptools-compatibility|setup_sidecar|print-setuptools-wheel|restates_the_version|require-hashes)' "$f"
  fi
done

Length of output: 93212


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== Raw release workflow install step (make control characters visible) =='
sed -n '174,188l' .github/workflows/release.yml
echo '== release.yml diff for that step =='
git diff 38b71fed7ddce31e2ec625b1dd32a3ae3e623ae2 4de6d5312e7237bbf3d31bc6ba5e43dca1b8f10d -- .github/workflows/release.yml | sed -n '1,180p'

echo '== setup_sidecar parser and CLI verification implementation =='
sed -n '115,215p' scripts/setup_sidecar.py

echo '== Parser tests, including all rejected fixtures =='
sed -n '80,205p' tests/test_setup_sidecar.py

echo '== Local packaging instructions and installer wheel consumption =='
sed -n '1,100p' packaging/README.md
sed -n '1,120p' packaging/scripts/post_install.sh
sed -n '1,130p' packaging/scripts/post_install.bat

echo '== PR diff for README and installer scripts =='
git diff 38b71fed7ddce31e2ec625b1dd32a3ae3e623ae2 4de6d5312e7237bbf3d31bc6ba5e43dca1b8f10d -- \
  packaging/README.md packaging/scripts/post_install.sh packaging/scripts/post_install.bat

echo '== Search for documented executable packaging commands and compatibility-file references =='
rg -n -i -C 3 \
  '(micromamba create|pip (download|install).*setuptools|SETUPTOOLS_WHEEL|setuptools-compatibility\.txt)' \
  -g '*.md' -g '*.rst' -g '*.txt' -g '*.sh' -g '*.bat' -g '*.yml' -g '*.yaml' \
  -g '!packaging/setuptools-compatibility.txt' .

Length of output: 39584


@bioedca I found two issues in the first round.

  1. Blocker — release.yml will fail before it builds tMAVEN.
    .github/workflows/release.yml Line 183 contains a literal \n:

    python -m pip install --require-hashes --only-binary=:all: \n            -r packaging/setuptools-compatibility.txt

    This is not a shell line continuation. Bash passes n as an argument to pip, so the newly added install step fails (or attempts to treat n as an unhashed requirement) before pip wheel --no-build-isolation runs. Replace the literal \n with an actual newline after the trailing backslash, as in packaging.yml.

  2. High — packaging/README.md is still an operational unpinned fifth consumer.
    Lines 55-76 provide a local installer-build recipe that:

    • adds "setuptools<81" directly to the micromamba build environment;
    • downloads it with an unpinned, unhashed pip download ... "setuptools<81" command; and
    • exports SETUPTOOLS_WHEEL=staging/setuptools-*.whl.

    That recipe can still create exactly the non-reproducible artifact this PR eliminates, and it requires a future bump to find another bound. Update it to install/download from packaging/setuptools-compatibility.txt, verify/stage setup_sidecar.py --print-setuptools-wheel, and export the exact resulting filename.

The parser itself appears fail-closed for the extra forms called out: its re.fullmatch rejects non-SHA-256 hashes, markers, inline comments, and extra requirements. The workflow’s empty-expected case also fails rather than silently matching: -f packaging/staging/ is false and the subsequent verification rejects it.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

…h consumer

Both from CodeRabbit's round-1 review, and both would have surfaced first as a broken RELEASE -
neither packaging workflow runs on a pull request, so nothing here executes the steps this PR
changes.

**A literal `\n` reached release.yml.** The generated step read `--only-binary=:all: \n            -r
...`, so bash would have handed pip an argument `n`. The YAML parses, actionlint sees a valid string,
and no CI job runs it. Now a real continuation, and a test rejects the shape - keyed on a FAILED
CONTINUATION (`\n` followed by continuation indentation) rather than the two characters, because
`printf '### %s\n%s\n\n'` in the same file is correct and flagging it would make the guard a nuisance
that gets deleted.

**There were FIVE consumers, not four.** `packaging/README.md` documents a runnable local build
recipe that still added `"setuptools<81"` to the build env, downloaded it unpinned and unhashed, and
exported `staging/setuptools-*.whl`. A documented command is a consumer - it gets copied and run -
and that recipe could still produce exactly the unreproducible artifact this change removes. It now
installs and downloads from the one source, verifies the staged bytes, and exports the exact name.
The contract test covers it, which is what would have caught it.

The single-source claim in the PR body was wrong when written: I asserted four homes after the test
found the fourth, and stopped looking.
@bioedca

bioedca commented Jul 30, 2026

Copy link
Copy Markdown
Owner Author

Round 1 answered at e2c4348. Two blocking findings from CodeRabbit, both fixed — and both would
have surfaced first as a broken release, because neither packaging workflow runs on a pull
request. That is precisely the residual the PR body flagged as unverifiable here, and it is sobering
that the review found two real defects inside it.

1. A literal \n reached release.yml. The step read
--only-binary=:all: \n -r ..., so bash would have passed pip an argument n. The YAML
parses, actionlint sees a valid string, and no CI job executes it. Now a real continuation.

The test I added is keyed on a failed line continuation\n followed by continuation
indentation — not on the two characters. printf '### %s\n%s\n\n' in the same file is correct, and a
guard that flags it becomes a nuisance that gets deleted rather than a guard.

The cause is worth naming: the step was produced by a Python script writing YAML, and one level of
backslash was consumed. I wrote a note to myself about that exact failure mode earlier today and then
did it again — which is a decent argument that generated workflow content needs a test, not a habit.

2. There were FIVE consumers, not four. packaging/README.md documents a runnable local build
recipe that still added "setuptools<81" to the build env, downloaded it unpinned and unhashed, and
exported staging/setuptools-*.whl. A documented command is a consumer — it gets copied and run
— and that recipe could still produce exactly the unreproducible artifact this PR removes. It now
reads the one source, verifies the staged bytes, and exports the exact name.

The single-source claim in my PR body was wrong when I wrote it. The contract test found a fourth
home, I reported "four, not the three the issue names", and stopped looking. The test now covers all
five, which is what should have found it.

Thank you also for confirming the two things I asked about and could not test: the parser is
fail-closed for the extra forms, and the empty-expected case fails rather than matching.

No further review is requested. Round 1 of 2 is spent; this is the answer to it.

@codex has not reported on this PR. Under the routing rule high takes both providers, so if Codex
returns before the checks are green I will fold its findings into this same round rather than
opening a second.

@chatgpt-codex-connector

Copy link
Copy Markdown

Review Finding

P1 — The “fifth consumer” fix leaves the removal contract and public documentation stale.

The compatibility file still instructs maintainers to delete the file and “its three consumers.” Following that instruction would leave the newly recognized runnable packaging/README.md consumer referring to a deleted requirements file. packaging/setuptools-compatibility.txtL33-L35

The standalone handoff repeats the same incorrect count and lists only the script and two workflows, omitting the local packaging recipe. docs/idealize/standalone-tmaven-handoff.mdL35-L40 docs/idealize/standalone-tmaven-handoff.mdL48-L50

Additionally, the packaging README still says the source path uses scripts/setup_sidecar.py’s SETUPTOOLS_PIN, but this PR removes that constant in favor of SETUPTOOLS_REQUIREMENTS. packaging/README.mdL25-L29

Please update both removal instructions to include the README recipe—preferably by naming all consumers rather than retaining another fragile count—and replace the obsolete SETUPTOOLS_PIN reference. Because these statements describe the single-source/removal contract introduced by this PR, the stale documentation falsifies that contract and is blocking under the repository’s review severity floor.

Checks

  • git show --format=fuller --no-ext-diff HEAD -- .github/workflows/release.yml packaging/README.md tests/test_setup_sidecar.py
  • git diff b76b45e..HEAD -- .github/workflows/packaging.yml .github/workflows/release.yml scripts/setup_sidecar.py tests/test_setup_sidecar.py packaging/README.md packaging/setuptools-compatibility.txt packaging/construct.yaml docs
  • pytest -q tests/test_setup_sidecar.py — 22 passed, 1 failed because test_status_prefix_matches_runner could not import the local tether package (ModuleNotFoundError).

View task →

@bioedca

bioedca commented Jul 30, 2026

Copy link
Copy Markdown
Owner Author

sidecar / parity failed once here and passed on a re-run of the identical commit. Recording the
control rather than just re-running, because "it went green the second time" is not a finding.

commit what changed run 1 run 2
4de6d53
e2c4348 one release.yml line, a README, a test

Nothing in e2c4348 is read by an ebFRET fit, and the same commit produced both answers — so the
variance is in the test, not this diff. And it is not marginal: Viterbi agreement 0.7568 against
a 0.9034 floor is a different fit, not a tolerance needing a hair's loosening.

Filed as #311, with the explicit note that widening the frozen band to make it pass is the one
thing not to do.

This PR is not blocked on it. It is blocked on @codex, which has not reported — risk is high, so
both providers are needed and CodeRabbit's leg is answered. Waiting rather than merging on one.

@chatgpt-codex-connector

Copy link
Copy Markdown

Summary

Testing

  • git diff --check
  • ⚠️ python -m pytest -q tests/test_setup_sidecar.py — 22 passed; one test could not import the uninstalled local tether package.
  • ⚠️ PYTHONPATH=src python -m pytest -q tests/test_setup_sidecar.py — 22 passed; one test could not import the unavailable numpy dependency.
  • ⚠️ QT_QPA_PLATFORM=offscreen PYTHONPATH=src python -m pytest -m "not large and not sidecar and not deep" — collection was blocked by missing environment dependencies, including numpy and yaml.
  • ⚠️ mkdocs build --strictmkdocs is not installed in the environment.
  • ⚠️ pre-commit run --all-filespre-commit is not installed in the environment.
  • git status --short --branch — working tree clean after commit.

View task →

Codex's round-1 finding, and its recommendation is better than the fix it asked for: a COUNT goes
stale the moment one is added, which is precisely what happened. The requirements file still said
"its three consumers" while the contract test had already found a fourth and CodeRabbit a fifth, so
following the removal instruction would have left `packaging/README.md` pointing at a deleted file.
The consumers are now listed by name, in the file itself, with the test that enforces the list named
beside them.

It also surfaced four dangling references to `SETUPTOOLS_PIN`, a constant this PR REMOVES:
`post_install.sh`, `post_install.bat`, a `packaging.yml` comment, and - worst - `docs/troubleshooting.md`,
which told a user debugging a failed sidecar that the script is "the ONLY place the pin lives". Each
sent a reader looking for something that is not there.

Removing a constant is easy; removing every sentence that explains it is what gets missed, so that
is now a test rather than a sweep - repository-wide rather than over a consumer list, because the
failure mode is exactly a file nobody thought of as a consumer.
@bioedca

bioedca commented Jul 30, 2026

Copy link
Copy Markdown
Owner Author

Answered at 353fb25. @codex — correct on all three, and your recommendation was better than the
fix you asked for.

"Name the consumers rather than retaining another fragile count." That is the actual lesson. The
file said "its three consumers" while the contract test had already found a fourth and CodeRabbit a
fifth — so following the removal instruction would have deleted the requirements file and left
packaging/README.md pointing at nothing. They are now listed by name, in the file itself, with
the test that enforces the list named beside them. A count was always going to go stale; a list fails
loudly when the test disagrees with it.

The SETUPTOOLS_PIN reference was worse than one stale line — there were four. Beyond the
packaging/README.md one you cited: post_install.sh, post_install.bat, a packaging.yml
comment, and docs/troubleshooting.md, which tells a user debugging a failed sidecar that
setup_sidecar.py is "the only place the pin lives". That is the worst of the four — it is read
exactly when someone is already lost, and it now sends them to a constant that does not exist.

Removing a constant is easy; removing every sentence that explains it is what gets missed. So that is
now a test, scanning the repository rather than a consumer list — because the failure mode is
precisely a file nobody classified as a consumer.

On your third check: pytest tests/test_setup_sidecar.py failing on
test_status_prefix_matches_runner with ModuleNotFoundError: tether is an environment artifact, not
a regression — that test imports the installed package, and the repository's own gate installs it.
It is green on the 3-OS test matrix here.


Both providers have now answered this round and the findings did not overlap. CodeRabbit found a
literal \n that would have broken a release and the fifth consumer; Codex found the stale removal
contract and the dangling constant. Neither found the other's.

sidecar / parity failed once and passed on a re-run of the identical commit; the control is in the
comment above and the nondeterminism is filed as #311.

No further review is requested. Round 1 of 2 is spent; this is the answer to it.

@chatgpt-codex-connector

Copy link
Copy Markdown

Acknowledged. The Round 1 findings were addressed at 353fb25, both review providers have responded, and the trigger explicitly requests no further review. I made no code changes, commit, or pull request.

View task →

@bioedca
bioedca enabled auto-merge (squash) July 30, 2026 11:03
@bioedca
bioedca merged commit e92b49f into main Jul 30, 2026
16 checks passed
@bioedca
bioedca deleted the agent/issue-218 branch July 30, 2026 11:04
bioedca added a commit that referenced this pull request Jul 30, 2026
…se binary

Maintainer resolved the escalation: `gh attestation verify --bundle`, shipped as a
pinned+hashed `extra_files` artifact, bundle fetched from the public attestations API,
NOT from conda-forge and NOT in the lock. Everything decision 2 requires substantively -
attestation verification, checksum necessary-but-not-sufficient, refuse-and-report, no
"install anyway" - is unchanged. The call mechanism and the delivery mechanism changed.

THE ESCALATION PRESENTED A FALSE BINARY. It framed the choice as "bundle into the base
lock" vs "first pip entry in the base lock". A third route was already proven in this
repo: construct.yaml `extra_files` with a pinned, hashed, single-source-of-truth artifact
- the #306 setuptools-wheel pattern - which ships a third-party binary with full
provenance and never re-solves the 1192-entry lock. The re-lock this record insisted must
be "a separate, deliberate PR" is not owed at all. That was the single biggest cost the
escalation was about.

It also decouples the verifier from the conda-forge feedstock, which matters more than it
sounds: cosign there is 3.0.4 against upstream 3.1.2, four releases and ~6 months behind,
one maintainer. Pin-and-holding a SECURITY tool to a channel that does not patch it
promptly inverts the point of pinning.

gh OVER cosign, on how much security logic Tether must author. cosign is a signature
engine, not a sufficient verifier: it ignores `--type` entirely (a nonsense predicate URI
still returned "Verified OK", exit 0), binds the digest but never the subject filename,
and makes the caller hand-author the Fulcio identity regex - where a wrong regex silently
accepts any Sigstore-signed artifact from any repository. gh encodes GitHub's own policy
via --repo/--signer-workflow/--source-ref.

AND A CORRECTION THIS RECORD OWED. An earlier draft said gh was disqualified because it
demands a token. That is true only of the path where gh fetches the bundle itself. Both
verified: the attestations REST endpoint is fully public (HTTP 200, complete sigstore
v0.3 bundle, no Authorization header), and `gh attestation verify --bundle` then verifies
with no credentials at all - proven end-to-end against the real 624MB v1.0.0-rc1 arm64
installer. No release-pipeline change is needed either; the bundle comes from the API,
not from a release asset.

Four app-side requirements are now recorded as requirements, not polish, because no tool
here is sufficient alone: bind the subject FILENAME as well as the digest (one bundle
covers all four installers); read the API response as explicit UTF-8 (cp1252 on Windows
corrupts the Rekor checkpoint em-dash and produces a false tamper verdict); pin gh
>= 2.67.0 (CVE-2025-25204 made this exact command FAIL OPEN); prefer the inline bundle
and tolerate bundle_url.

And one honest limit: decision 2 asks that the user be told WHY verification failed. That
is not achievable - gh collapses every outcome to exit 1, so a tampered artifact and a
dead network are indistinguishable. Recorded as a bounded split instead: ran-and-failed is
reported, could-not-run is a SILENT no-op, or the air-gapped promise breaks by a second
route. PRD 7.12 amended to match rather than mandating what cannot be built.

All six follow-ups are now filed and linked: #330 settings store, #332 gh bundling, #248
release query, #333 verification, #331 hand-off, and the privacy amendment inside #248.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

build(packaging): pin and hash the bundled setuptools wheel, with one source of truth

2 participants