Skip to content

the crates.io token exchange sends no user-agent and dies as a policy 403: name the org in every registry curl #907

Description

@CarlAllenn

Defect

edtf v1.3.2's registry legs failed three consecutive times (run
32681747423 + two re-runs) and the cause is the canon's, not the
repository's. Measured 2026-08-24 by a claims probe on an edtf branch
(runs 32684527127, 32684570428, 32684599155 — branch oidc-probe,
deleted after):

  • GitHub's OIDC mint is HTTP 200 with complete, correct claims
    (repository monumental-archive/edtf, environment publish,
    ID-stamped immutable sub) — the failure is registry-side.
  • publish-rust-crate.yml's exchange curl (line ~109) sends no
    meaningful User-Agent, and crates.io's protection layer 403s it with
    the data-access-policy boilerplate — a misleading, unattributed
    refusal that reads like a trust mismatch. With a User-Agent naming
    the org, the same exchange proceeds to real validation
    and answers
    HTTP 400 naming the probe's wrong filename and listing the FIVE
    configured publish.yml publishers — i.e. the repository's
    trusted-publishing config is correct and complete, and the header is
    the entire crates-side defect.
  • The npm leg fails ENEEDAUTH in the same runs. Boundary measured:
    the mint works (above), release-lab published npm 2026-08-21 through
    the identical workflow, and the package's trusted publisher was
    re-verified during the failures. The one measured delta: edtf pins
    node = "26.5.1", the lab pins 24.19.0 — the bundled npm differs,
    and npm's OIDC trusted publishing is npm-version-sensitive. Unproven;
    the build pins it by measurement.

Consequence recorded: v1.3.2's crates/npm legs are unreachable at that
tag forever (the tag's tree pins canon v1.60.0), so v1.3.2 is a partial
burn — GitHub release published with full evidence, registries not.
edtf's next release after the pin bump completes the arrival.

Decided build

Decided 2026-08-24, from the probe:

  • the crates exchange curl gains
    User-Agent: monumental-archive-publish (https://github.com/monumental-archive/.github)
    — an org-identifying UA per crates.io's own data-access policy, with a
    comment quoting the boilerplate 403 so the next reader recognises the
    block if the policy tightens again.
  • sweep every curl in the publish/release workflows and release/
    scripts that targets a third-party registry (crates.io, npmjs, zenodo)
    for the same gap; GitHub-API curls keep their existing shape.
  • the npm leg: measure npm at node 26.5.1 vs 24.19.0 for OIDC trusted
    publishing (the CLI's own debug log names the step); fix belt-side if
    it is a canon assumption, or file the edtf pin correction if it is the
    repo's node pin — whichever the measurement says. Do not ship a token
    fallback: the workflow's deliberate ENEEDAUTH-over-weaker-auth stance
    stays.

Rejected, recorded: emailing crates.io for an allowlist (the header is
the documented contract); retrying v1.3.2 (the tag's pin cannot see any
fix by construction).

Canon consequence

Release-path change: rides the next canon release, no lab leg (standing
ruling — and the lab could never have caught this: rust-crate is not in
the lab's class list, the paths-proven-only-by-real-repos family). Every
consumer's registry legs inherit the header at their pin bump.

Done when

Sequencing

Immediate — it blocks edtf's arrival on both registries. Canon release
after merge; edtf pin bump; edtf release next. Refs #833, #834,
release-lab#262 (proof ledger).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions