Maintaining acdp-registry-rs
This page is for the people who hold admin on this repository: how CI decides what blocks a merge, how branch and tag protection are kept honest, how a release happens, and the few scheduled jobs whose failures land on nobody's PR. Contributors want CONTRIBUTING.md; operators deploying the registry want OPERATIONS.md.
One rule runs through all of it: the committed files and the live settings are the
sources of truth, never a copy in prose. This page names files and shows how to read
live state with gh api; it deliberately does not list required-check names, because
every transcribed copy of that list in this repository has gone stale.
Required and advisory checks
The committed baseline for main's protection is
.github/required-checks.json:
required— the status checks that block a merge. Each entry pinsapp_idto 15368 (GitHub Actions). An unpinned required context is satisfied by any app withstatuses: write(the deps bot, Vercel), so an unpinned entry is a hole, not a shorthand.strict— a PR branch must be up to date withmainbefore it merges.advisory_pending— checks that run on every PR but do not block. A red advisory check leaves the merge button green, which is why some gates are run twice (see the upgrade-notes gate under Release flow). Today the only one iscargo-vet: it stays advisory until a maintainer deliberately promotes it in a PR of its own, and the runbook excludes it (see Supply-chain audits).msrv,rustdoc,coverage,docker (build + smoke)andmutants pinswere promoted on 2026-10-05.enforce_admins,tag_ruleset,pending_settings— the settings a maintainer applies by hand; see Current protection settings.
Read the live setting rather than trusting the file or this page:
gh api repos/agentcontextdistributionprotocol/acdp-registry-rs/branches/main/protection \
--jq '{checks: .required_status_checks.checks, strict: .required_status_checks.strict, enforce_admins: .enforce_admins.enabled}'The PR-time guard
.github/scripts/required_checks_guard.py runs in the lint job on every PR. It reads
the committed baseline (not the live setting) and, for every name in required and
advisory_pending, requires that the name resolves to exactly one workflow job (its
name:, else its id; caller / callee for a local reusable workflow). It rejects:
- a name that resolves to no job, or to more than one (fails loudly; never skipped);
- a job whose workflow is not triggered by
pull_request, or whosepull_requesttrigger has apaths,paths-ignoreorbranches-ignorefilter, abranchesfilter that does not matchmain, or atypesfilter missingopened,synchronizeorreopened— some PRs would never get the check; - a
branchespattern using?,+,[or\(GitHub's filter syntax and the guard's matcher disagree on those, so the guard fails closed); - a job-level
if:orneeds:(a skipped job reports success),continue-on-error, a matrix, or${{ }}inname:(the reported name would not be the literal one); - a
requiredentry withoutapp_id, or pinned to anything but 15368; - shape errors in the file itself: unknown or missing top-level keys, duplicate names,
a malformed
tag_ruleset.
The practical consequence: renaming a required job fails lint in the PR that renames
it. The fix is to change the job name, the baseline and the live setting in the same
window, or every later PR waits forever on a context that never reports.
Run it locally (PyYAML is preinstalled on GitHub runners but not on a stock macOS
python3):
pip3 install --user pyyaml
python3 .github/scripts/required_checks_guard.py --check
python3 .github/scripts/required_checks_guard.py --self-test # negative controlsThe daily drift check
branch-protection-drift.yml runs
daily (05:23 UTC) and on workflow_dispatch. It mints a read-only token from the
acdp-deps-bot App (administration: read, metadata: read) and runs
.github/scripts/protection-drift.sh --fetch, which compares the live settings with the
baseline:
- Required checks and
strict— always a hard failure on drift: a missing check, a check pinned to a different app, an unpinned or duplicated live context, an extra live check the baseline does not list, orstrictchanged. enforce_adminsand the tag ruleset — while the baseline says"pending_settings": true, a mismatch is a::warning::and the job stays green; set tofalse, the same mismatch fails. When everything already matches whilepending_settingsis stilltrue, the job emits a notice telling you to flip it.- Ruleset
bypass_actors— the API may not return the bypass list to a read-only token. When the baseline expects a non-empty list and the live one is absent, null or empty, the job reports it as unverifiable (::notice::) rather than failing; check the bypass list by eye after any change. - A failed GET is reported as "could not read", never as "nothing configured".
Most failure messages carry the command that restores the baseline (a few, such as a duplicate tag ruleset or a failed GET, only say what to check). To prove the
hard-fail path against the live settings without merging a red main, push a branch
whose baseline sets pending_settings to false and run
gh workflow run branch-protection-drift.yml --ref with that branch name. The script's
own --self-test runs in lint on every PR.
Current protection settings
Read with the GETs below on 2026-10-04, before the settings were applied (see Applied 2026-10-05 below for the current state). Re-read them before acting on this section — it is a snapshot, the GETs are the truth.
R=repos/agentcontextdistributionprotocol/acdp-registry-rs
gh api $R/branches/main/protection \
--jq '{checks: .required_status_checks.checks, strict: .required_status_checks.strict, enforce_admins: .enforce_admins.enabled}'
gh api "$R/rulesets?includes_parents=false"At that time (before 2026-10-05):
- the live required checks matched
requiredin the baseline exactly, every one pinned to 15368, withstricton — the daily drift runs were green; enforce_adminswas false and the repository had no rulesets, while the baseline recordsenforce_admins: trueand theprotect-release-tagstag ruleset withpending_settings: true.
Applied 2026-10-05. After the v0.3.0 release, the tag ruleset protect-release-tags
(id 24481366, sole bypass actor the acdp-deps-bot App 4260407, confirmed as the identity
that created the v0.3.0 tags) and enforce_admins were applied, and msrv, rustdoc,
coverage, docker (build + smoke) and mutants pins were promoted to required;
cargo-vet stays advisory. The probe tag creation was rejected with HTTP 422. The baseline
now has pending_settings: false. The runbook below is kept for re-applying after a
rollback or in a fork.
Runbook: enforce_admins, the tag ruleset, and promoting advisory checks
Nothing automated applies these settings; a maintainer runs the commands below with an
admin gh login. The mutating commands (every --method POST, PATCH, PUT or DELETE) were run
against this repository on 2026-10-05 (see Current protection settings);
they stay here for re-applying after a rollback or in a fork.
Order matters: settings first, then the baseline PR. Prepare a PR that moves the
advisory_pending names except cargo-vet into required (each with
"app_id": 15368), leaves cargo-vet in advisory_pending, and sets
pending_settings to false, but merge it only after the settings are applied, so its
own head shows the promoted checks reporting. Until it merges, the drift job reports the
promoted checks as "already required live but still advisory_pending" — a warning, not a
failure.
What the settings are:
enforce_admins— protection binds admins too. Set through its dedicated endpoint, never through a fullPUTof the protection object (which resets every field the body omits).- Tag ruleset
protect-release-tags— the body is the baseline'stag_ruleset: every tag (~ALL), rulescreation,update,deletion,non_fast_forward, active, with exactly one bypass actor: theacdp-deps-botApp (Integration,always), which release-plz uses to create release tags. There is no admin bypass; break-glass is disabling the ruleset. Because it covers every tag, it also blocks anyone (admins included) from creating, moving or deleting tags by hand, including the oldarchive/*style tags. - Promoting checks — a read-modify-write of
required_status_checkswith the object form{strict, checks: [{context, app_id}]}viaPATCH. Never usePOST .../required_status_checks/contexts: it appends unpinned contexts that any app with status-write access could satisfy.
Pre-flight (all must hold)
R=repos/agentcontextdistributionprotocol/acdp-registry-rs
gh pr list --repo agentcontextdistributionprotocol/acdp-registry-rs # no open PRs you care about mid-flight
gh run list --repo agentcontextdistributionprotocol/acdp-registry-rs --workflow release-plz.yml --limit 3 # nothing in progress
gh api $R/branches/main/protection \
--jq '{checks: .required_status_checks.checks, strict: .required_status_checks.strict, ea: .enforce_admins.enabled}'
gh api "$R/rulesets?includes_parents=false" # expect [] before the first apply
# The bypass actor id in the baseline must be the deps bot's App id:
gh api orgs/agentcontextdistributionprotocol/installations \
--jq '.installations[] | select(.app_slug=="acdp-deps-bot") | .app_id'
jq '.tag_ruleset.bypass_actors' .github/required-checks.json
# Vercel holds administration:write on the repos it is installed on; it must NOT be
# installed on this one. Listing an installation's repos needs the read:user scope.
VERCEL=$(gh api orgs/agentcontextdistributionprotocol/installations \
--jq '.installations[] | select(.app_slug=="vercel") | .id')
gh auth refresh -h github.com -s read:user
gh api "user/installations/$VERCEL/repositories" --jq '.repositories[].full_name'Also confirm the acdp-ci script described below will not be applied to this repository during or after the window.
Snapshot
SNAP=$(mktemp -d) # outside the worktree
gh api $R/branches/main/protection > "$SNAP/protection-before.json"
gh api "$R/rulesets?includes_parents=false" > "$SNAP/rulesets-before.json"
echo "$SNAP" # keep this path; rollback reads from itApply
# 1. Tag ruleset, built from the baseline (rules become objects), id captured.
RS=$(jq '.tag_ruleset | .rules |= map({type: .})' .github/required-checks.json \
| gh api --method POST $R/rulesets --input - --jq .id)
gh api $R/rulesets/$RS --jq '{enforcement, bypass_actors, rules: [.rules[].type]}'
# 2. Probe: an admin creating a tag must now be REJECTED (HTTP 422).
# If it succeeds, delete the ref at once and stop:
# gh api --method DELETE $R/git/refs/tags/zz-ruleset-probe
gh api --method POST $R/git/refs \
-f ref=refs/tags/zz-ruleset-probe -f sha="$(git rev-parse origin/main)"
# 3. enforce_admins, through its dedicated endpoint.
gh api --method POST $R/branches/main/protection/enforce_admins --jq .enabled
# 4. Promote the advisory checks: live checks + the baseline's advisory_pending
# MINUS the names in KEEP_ADVISORY, every one pinned (unique_by makes a re-run
# harmless). cargo-vet stays advisory: promoting it would block every Dependabot
# and bump-acdp PR until someone regenerates the exemptions. Inspect the body
# first: it must not contain cargo-vet.
KEEP_ADVISORY='["cargo-vet"]'
gh api $R/branches/main/protection/required_status_checks \
| jq --slurpfile b .github/required-checks.json --argjson keep "$KEEP_ADVISORY" \
'{strict, checks: (((.checks | map({context, app_id}))
+ ($b[0].advisory_pending - $keep | map({context: ., app_id: 15368})))
| unique_by(.context))}' \
> "$SNAP/rsc-new.json"
jq . "$SNAP/rsc-new.json"
gh api --method PATCH $R/branches/main/protection/required_status_checks \
--input "$SNAP/rsc-new.json"Verify
-
Merge the prepared baseline PR (it must pass the newly required checks).
-
Dispatch the drift job and watch it go green with no warnings:
gh workflow run branch-protection-drift.yml --repo agentcontextdistributionprotocol/acdp-registry-rs sleep 5 # let the dispatched run register before listing gh run list --repo agentcontextdistributionprotocol/acdp-registry-rs --workflow branch-protection-drift.yml --limit 1 gh run watch --repo agentcontextdistributionprotocol/acdp-registry-rs RUN_ID # RUN_ID from the line above -
Confirm the ruleset's bypass list by eye (
gh api $R/rulesets/$RS), since the drift token may not see it. -
Watch the next release-plz run: its tags must be created (positive proof that the bot bypass works), and the next dependabot or release PR must show the promoted checks reporting and still merge.
Rollback and break-glass
# Admins no longer bound by protection:
gh api --method DELETE $R/branches/main/protection/enforce_admins
# Tag ruleset off (break-glass; keeps it for re-enabling) or gone:
echo '{"enforcement":"disabled"}' | gh api --method PUT $R/rulesets/$RS --input -
gh api --method DELETE $R/rulesets/$RS
# Required checks back to the snapshot (pinned object form again):
jq '{strict: .required_status_checks.strict,
checks: (.required_status_checks.checks | map({context, app_id}))}' \
"$SNAP/protection-before.json" \
| gh api --method PATCH $R/branches/main/protection/required_status_checks --input -Then revert the baseline JSON in a PR to match, or the drift job fails on the next run
(context drift is never a warning). If RS is lost, find the id with
gh api "$R/rulesets?includes_parents=false".
The acdp-ci standardize.sh hazard
The org's shared CI repository, acdp-ci, has a script, scripts/standardize.sh,
that rewrites branch protection across the org's repositories. Its apply path is
dangerous for this repository and must not be run against it until it is changed:
- for repositories that have required checks, the protection body it sends sets
enforce_adminsto false, silently undoing the runbook above; - it sends checks as bare context names, which unpins every required check
(
app_idbecomes null — "any app may satisfy this"); - its list of this repository's checks is hand-maintained in that script, so once
advisory checks are promoted here, its read-only
--checkmode reports them as drift, and an apply with--allow-check-removalwould drop them.
Its --check (alias --dry-run) mode is read-only and safe to run. The durable fix
belongs in acdp-ci: declare this repository's settings with enforce_admins on and
pinned checks, or read them from this repository's .github/required-checks.json. The
same repository's reusable auto-merge.yml reads classic branch protection, which is one
reason this repository keeps classic protection for main rather than moving it to a
ruleset.
Release flow
- Every push to
mainrunsrelease-plz.yml.release-plz.tomlsetsgit_only = true(previous versions come from git tags, not crates.io) andpublish = false(nothing is published to crates.io; do not flip it — that is a one-way door). release-plz opens or updates a release PR that bumps versions and per-crate changelogs from Conventional Commit subjects. - Merging the release PR makes release-plz create one tag per crate, named
{{ package }}/v{{ version }}, plus GitHub Releases. It authenticates with a token minted from theacdp-deps-botGitHub App (ACDP_BOT_APP_ID/ACDP_BOT_PRIVATE_KEY), because tags and PRs created with the defaultGITHUB_TOKENdo not trigger workflows. The same App is the tag ruleset's only bypass actor. - The server crate's tag (
acdp-registry-server/v*) triggersdocker.yml, which builds thestorage-pgimage and pushes it to GHCR with the semver tags (full version and major.minor). Pushes tomainpublishmain,latestandsha-tags; PRs build and smoke-test but never push. - STATUS.md names the stable minor line ("0.4.x"); touch it only when a release changes the minor, so it does not drift on every patch.
Gates a release PR must pass:
- Upgrade notes.
docker/assert-upgrade-notes.sh --checkrequires a section in UPGRADING.md for the version being built, even if it says nothing changed. It runs in two places: inside therustfmtjob, which is required and therefore blocks, and indocker.yml, insidedocker (build + smoke), which is also required and therefore blocks (that copy also covers the tag push, whichci.ymlnever sees). - Railway image tags.
conformance_gaterequiresdocker/RAILWAY.mdto name the major.minor of the newest## X.Y.Zsection ofdocs/UPGRADING.md(or the workspace version, if newer). For a breaking release, merge the UPGRADING section and the RAILWAY.md tags onmainbefore the release PR: release-plz closes and reopens a release PR that carries a human commit, so the page cannot be fixed on its branch.
CI behaviour worth knowing
- Coverage (
coveragejob, required). Onecargo llvm-covnumber merged across four legs — the workspace, the server withplayground, the server's Postgres integration suite, and the server on the memory backend — with--fail-under-lines 88. The floor isfloor(min) - 1over at least three CI runs of the merged job; re-derive it the same way, recording the runs in the commit, whenever the code or the set of legs changes materially. The job is required, so the floor blocks a merge. ACDP_REQUIRE_PG. Postgres-backed tests skip (printing a line) whenACDP_REGISTRY_TEST_PG_URLis unset. WithACDP_REQUIRE_PGset a missing URL is a hard failure instead (most tests treat any value as set; the auth crate'spg_revocationtest only fails on exactly1, and otherwise skips without printing). CI sets it in thetestsandcoveragejobs, but the requiredtestsjob does not pass the Postgres URL to the workspace run, so the auth Postgres test runs only in the requiredcoveragejob.- Docker smoke.
docker (build + smoke)builds the image, boots it against Postgres and checks health, then boots the documented compose quickstart throughdocker/assert-quickstart-boots.sh(as shipped, and again with auth enabled), and runs that script's self-test. - Yanked crates.
deny.tomlsetsyanked = "deny"andcargo-denyis required, so an upstream yank can block every merge with no commit here. Fix it withcargo update -pon the named crate, one package at a time; do not add anignoreentry for a yank. - Conflict markers.
no_tracked_file_contains_a_conflict_markerfails the build on any merge-conflict marker (including the diff3 base marker) in any tracked file. - Documentation guards. Most doc-truth guards live in
crates/acdp-registry-server/tests/conformance_gate.rs(documented wire codes and routes, the docs index, relative links resolving, sibling-repository links pinned (sibling_repo_links_are_pinned), no line-number citations in operator docs, Railway tags, and more);metrics_integration.rschecks the documented rate-limit scopes. Read the test names there rather than a list here.
Supply-chain audits (cargo vet)
cargo vet (job cargo-vet in ci.yml, advisory) requires every third-party crate
in Cargo.lock to be covered by one of:
- an audit of our own in
supply-chain/audits.toml(empty today); - an imported audit.
supply-chain/config.tomlimports acdp-rs'saudits.toml, where the protocol library's maintainers record full and delta audits of the crypto-critical crates it shares with this registry (ed25519-dalek, curve25519-dalek, signature, sha2, p256, ecdsa, elliptic-curve, rustls, ring, and their RustCrypto support crates), plus the Mozilla, Google and Bytecode Alliance sets acdp-rs also imports. The fetched sets are frozen insupply-chain/imports.lock, and CI runscargo vet --locked, so the verdict comes from the tree alone; - an exemption in
supply-chain/config.toml: everything else, as generated bycargo vet regenerate exemptions. That includes the registry-only crates (axum, sqlx, tokio and the rest of the server stack), the older RustCrypto line thatjsonwebtokenpulls in (for example digest 0.10, hmac 0.12, ed25519 2.x, pkcs8 0.10, sec1 0.7, spki 0.7), zeroize 1.9.0 (acdp-rs exempts it too), and theacdp-*crates from crates.io. An exemption means "trusted for now, not reviewed". It is a backlog, not a verdict. A crypto-critical crate may be exempted only at a version on the allow-list (see below).
The workspace's own crates carry audit-as-crates-io = false policies. release-plz has
publish = false, so they are not on crates.io; the policies keep cargo vet from
asking about them if that ever changes.
cargo-deny and cargo-vet answer different questions and neither replaces the other.
cargo-deny (required, configured by deny.toml) rejects crates with a known RUSTSEC
advisory, a yank, a disallowed license, a banned crate or an unknown source. It says
nothing about whether anyone has read the code. cargo vet records who reviewed which
version against which criteria (safe-to-deploy, safe-to-run), and it says nothing
about advisories: an audited version with a fresh advisory still passes vet, and
cargo-deny catches it. They share no configuration. An ignore in deny.toml does not
exempt anything from vet, and a vet exemption does not silence deny.
When cargo-vet goes red (a Dependabot bump, a bump-acdp PR, or a new dependency),
the job does not block the merge. Expect this on any change to Cargo.lock. A PR that
adds or bumps dependencies must refresh supply-chain/ in the same PR, even though the
job is advisory, so that main stays green and the next PR's vet result means
something. To make it green again, on the PR's branch:
cargo vet # lists what is unvetted and suggests audits
cargo vet regenerate exemptions # exempt the new versions (no review claimed)
cargo vet --locked # what CI runs
python3 .github/scripts/check_crypto_exemptions.py # the crypto guard belowcargo vet regenerate exemptions also drops exemptions that an audit now covers. Run
cargo vet regenerate imports occasionally to pick up new imported audits, acdp-rs's
included. The import tracks acdp-rs's main on purpose, so imports.lock is where new
trust arrives. Review its diff like an audit before committing it, whether it came
from regenerate imports or from a plain cargo vet without --locked. Check which
audits are new or changed and who certified them (who, criteria, version or delta,
and whether the notes say what was read). Check every new wildcard audit and
[[publisher.*]] or trusted-publisher entry: it vouches for future versions too. Check
that no import silently replaced an exemption you meant to keep reviewing. To record a real review instead, use
cargo vet certify <crate> <version> (or a delta from an audited version). The notes
should say what was read and what is not claimed, as acdp-rs's worksheets do. Each
acdp bump changes the acdp-* versions, so a bump-acdp PR always needs the
regenerate step until those crates are trusted some other way (for example a
cargo vet trust entry for their publisher, which is a maintainer decision).
Crypto-critical crates may not be quietly exempted
cargo vet passes an exempted crate exactly like an audited one, so the regenerate step
above would exempt a bump of ed25519-dalek just as readily as a bump of axum. The same
cargo-vet job therefore runs
check_crypto_exemptions.py after
cargo vet --locked (its self-test,
test_check_crypto_exemptions.py,
runs first). It reads three committed files:
supply-chain/crypto-critical.txt: the guarded crates. It mirrors acdp-rs'sscripts/crypto-critical.txt(the signing, hashing, curve and TLS crates whose audits this repository imports). When acdp-rs adds a crate there, add it here.supply-chain/crypto-exemptions-allowed.txt: the exact (crate, version) pairs that may stay exempted for now, each with a tracking issue and a reason. Today these are the older RustCrypto line (acdp-rs never locks those versions, so its audits will not cover them) and zeroize 1.9.0 (exempt in acdp-rs too). A version acdp-rs already audits but ourimports.lockpredates does not belong here: refresh the imports instead (cargo vet regenerate imports, thencargo vet regenerate exemptions).supply-chain/config.toml: the exemptions themselves.
The guard fails when a guarded crate is exempted at a version that is not on the
allow-list, and when an allow-list entry is no longer exempted. The second rule means
the allow-list only shrinks: the PR whose audit or import retires an exemption must
also delete its line. A name in crypto-critical.txt that is not in Cargo.lock, or an
exemption written in a shape the guard cannot read, is an error rather than a pass.
When it fails on a bump, do not reach for the allow-list first. Get the version
audited: wait for (or ask for) an acdp-rs audit and pull it in with
cargo vet regenerate imports, or record one here with cargo vet certify (a delta from
the audited version is usually small). Adding an allow-list line is a reviewed decision
that a crypto crate stays unreviewed for now. It needs a tracking issue that ends it.
The guard is advisory because the job is. Its fixture tests also run in the required
tests job, so a broken guard script fails a required check. Only its verdict on this
repository's own supply-chain/ is advisory. It reads config.toml with Python's
tomllib (3.11+, as on CI). On older Pythons it falls back to a line reader that
refuses any exemption spelling other than cargo-vet's own, and the self-test checks
that both readers agree on the real file. It reads config.toml rather than
cargo vet's JSON (acdp-rs's scripts/check-crypto-vet.sh reads the JSON), so it says
"exempted or not", not "fully audited or not". A crate trusted through a
[[trusted.*]] or wildcard entry is not an exemption and passes.
The cargo-vet version is pinned in ci.yml (installed with cargo install --locked
because the pinned taiki-e/install-action has no manifest for it). Keep it in step with
acdp-rs's pin, so both repositories read the same imports.lock format.
cargo-vet stays advisory until a maintainer decides otherwise. The runbook's
promotion step leaves it out on purpose. While the acdp-* crates and most of the graph
are exempted, every Dependabot and bump-acdp PR would be blocked until someone ran
cargo vet regenerate exemptions. Promoting it is a separate, deliberate change. Remove
cargo-vet from KEEP_ADVISORY in step 4. Move it from advisory_pending to required
in the baseline in the same PR. Have a plan for the acdp-* bumps first, such as a
cargo vet trust entry or a regenerate step in bump-acdp.yml.
Mutation oracle
mutants.yml runs cargo mutants over the scope
defined in .cargo/mutants.toml on a Monday cron (05:17 UTC) and on
workflow_dispatch. The mutation run itself never runs on PRs (about two hours); the
mechanical half of its pins is checked at PR time instead — see
PR-time pin check below.
The gate is exact, not a budget:
MUTANTS_EXPECTED_SCOPEmust equal the measured number of mutants — a shrinking scope would otherwise read as an improvement;- the set of surviving mutants must equal
MUTANTS_SURVIVORS, line for line. Each entry is a verbatimcargo mutants --listline, which includes the line and column of the mutation, so an ordinary edit that moves code in a scoped file changes the expected lines even when nothing new survives.
When it fails, re-measure and update both values in mutants.yml (and the per-file
counts in .cargo/mutants.toml) in one commit, with a reason for any survivor added:
cargo mutants --list | wc -l # total scope
cargo mutants --list | sed 's/:[0-9]*:[0-9]*:.*//' | sort | uniq -c # per-file splitThe ledgers of past runs, and which one is current, are indexed in docs/mutation-runs/README.md.
Any edit to MUTANTS_SURVIVORS must also commit the outcomes.json of the run that
produced it and point MUTANTS_PRIOR_LEDGER at that file. The classifier pairs a drifted
survivor against that ledger, and it refuses a ledger that is missing any committed line.
every_committed_survivor_is_in_the_prior_ledger in
crates/acdp-registry-server/tests/conformance_gate.rs runs the same check on every PR.
PR-time pin check (mutants pins)
Three of the ratchet's pins describe the mutant listing, not verdicts, and
cargo mutants --list parses source without building (well under a second).
mutants-pins.yml therefore checks them on every
PR that can move them, so the Monday run is not the first to find out:
MUTANTS_EXPECTED_SCOPEequalscargo mutants --list | wc -l;- the per-file table in
.cargo/mutants.tomlequals the listing's per-file split (and itstotalrow equals both the rows' sum andMUTANTS_EXPECTED_SCOPE); - every
MUTANTS_SURVIVORSline, and everyTimeoutmutant recorded inMUTANTS_PRIOR_LEDGER(the budgeted one), appears in the listing verbatim. A survivor that only moved is reported with the line it moved to.
The logic is .github/scripts/check_mutants_pins.py;
its unit tests (test_check_mutants_pins.py) run in the required tests job on every PR.
The cargo-mutants version is written once, on mutants.yml's install line
(tool: cargo-mutants@27.1.0); the PR job reads and installs that same version, and the
check requires it to equal the prior ledger's cargo_mutants_version, so the cron, the PR
check and the ledger cannot list with different tools. Bump it only with a re-pin. After a
green check the job inserts one blank line at the top of a scoped file that holds a cited
survivor or timeout line (chosen from the pins, not hard-coded) and requires the same
check to fail — a falsification against the real tree and tool. If no scoped file holds a
cited line, it skips the falsification with a notice. Run it locally with:
cargo mutants --list --colors never > /tmp/listing.txt
python3 .github/scripts/check_mutants_pins.py check --listing /tmp/listing.txtIt cannot see verdicts: a new survivor, or a killed one, still only shows up on the cron.
When it fails, either make the edit line-neutral in the scoped files or re-pin in the same
PR: run gh workflow run mutants.yml --ref <branch>, commit that run's outcomes.json as
the new MUTANTS_PRIOR_LEDGER, and update the scope, the table and MUTANTS_SURVIVORS
from it. Do not hand-edit a survivor line to the position the failure hints at without
that run: survivor lines are verdicts and must match the prior ledger, which
every_committed_survivor_is_in_the_prior_ledger checks.
When it runs. The path filter is inside the job, not on the trigger: the job always
starts, and its first step diffs the PR's merge commit against main (HEAD^1..HEAD).
Unless a scoped file (examine_globs), .cargo/mutants.toml, mutants.yml,
mutants-pins.yml, the script or its tests, or anything under docs/mutation-runs/
changed, the remaining steps are skipped and the job is green in seconds. It fails
toward running: a push to main, a diff that cannot be computed, a relevance step that
crashes, or any relevance answer other than an explicit false runs the check.
Required (promoted 2026-10-05) — and why that is safe. It is listed under
required in .github/required-checks.json. A trigger-level paths: filter or a
job-level if: would make the check never report on unrelated PRs, which
required_checks_guard.py rejects for both required and advisory_pending (a required
context that never reports blocks every PR forever). Gating at step level keeps the context
reporting on every PR, so the guard resolves it and it is safe to require. It was
promoted together with the other advisory checks by the runbook above; a red result now
blocks the merge, because a stale pin otherwise costs one red Monday run of the
mutation oracle.
Status: pinned at scope 350 from run 39071836771 (a workflow_dispatch on
feat/acdp-0.15.0 9cffe36, 117.3 min of the 180-minute cap). #435's federation proxy change
turned retrieve's four unviable Json return mutants into one caught
Ok(Default::default()); the seven survivors are the same mutants on shifted lines, and the
run's report is committed unchanged as the prior ledger. An earlier #435 run hit the cap
because two new tests waited out a real 5 s connect timeout per mutant; a test that waits on
the network is the first thing to check when a run overruns. (History: #432 pinned scope 353
from run 37972268890; #401 pinned scope 354 from run 37248684286 after removing the
lifecycle charge pre-flight and four caught mutants; #395 pinned scope 358
from run 37222567772 after #373-#376 added twelve; the 2026-09-28 run 36417581090 failed
after #341 shrank the scope from 351 to 346, and #371 re-pinned it the same way.) Edits to the four scoped files after a re-pin
must be line-neutral until the next one (mutants pins checks this on the PR). Check
gh run list --workflow mutants.yml --limit 3 for the current state.
Dependency and acdp bumps
Bot PRs are batched and merged on green, not hand-superseded (see
STATUS.md). bump-acdp.yml opens
the PR when the acdp-rs repository dispatches acdp-released (or on demand with a
version); auto-merge.yml merges Dependabot
patch and minor PRs once the required checks pass; the acdp bump PR arms auto-merge
itself unless the bump is breaking, which waits for a human. Both delegate to reusable workflows in acdp-ci, which owns
their behaviour. After an acdp bump, also check the acdp-rs guide links: the
sibling_repo_links_are_pinned guard accepts any tag or full SHA, so a valid but
stale pin passes CI. Re-point them (and the docs.rs versions) by hand in a docs PR, per
the Link convention. A cargo vet import refresh and the
crypto-critical exemption guard are covered under
Supply-chain audits. What this repository treats as
done, and what is deliberately deferred, is in STATUS.md and
DEFERRED.md.
Spec bumps
bump-spec.yml opens a PR that rewrites only the
ref: in .spec-pin; the PR arrives red on conformance until the digest and fixture
count are updated by hand — the steps are in
CONTRIBUTING.md.
A bump that changes schemas/json/ can also turn conformance red without touching a
fixture: served_capabilities_documents_satisfy_the_pinned_schema and
served_registry_documents_satisfy_the_pinned_schemas (in tests/conformance.rs) spawn
the built binary and validate what it serves (/.well-known/acdp.json with auth off, auth
on and every advertisable profile on, plus a publish response, a context, a search
response, a log checkpoint and an error body) against the new revision's schemas (#385).
The digest covers only fixtures, so that failure is the signal that a served document no
longer matches the spec; fix the document, not the test.
Docs that link into the spec repository at the pinned revision must be re-pointed to the
new ref: in the same bump PR. sibling_repo_links_are_pinned (in conformance_gate.rs)
requires every spec link's ref to equal the ref: in .spec-pin (the one exception is
SPEC_DOCS_REF in that test, for links under the spec's docs/ directory, which is
updated by hand), so a bump PR also
fails the tests and conformance (spec fixtures) jobs — listing each stale link —
until they are re-pointed. From the
bump branch, with origin/main still at the old pin:
old=$(git show origin/main:.spec-pin | sed -n 's/^ref: //p')
new=$(sed -n 's/^ref: //p' .spec-pin)
[ -n "$old" ] && [ -n "$new" ] || { echo "could not read the old or new ref"; exit 1; }
git grep -lE "agentcontextdistributionprotocol/agentcontextdistributionprotocol/(blob|tree|raw)/$old" \
-- '*.md' 'config/*.toml' 'docker/*' ':!plans' ':!crates/*/CHANGELOG.md' \
':!DECISIONS.md' ':!ASSUMPTIONS.md' ':!docs/ENGINEERING-LOG.md' \
| xargs perl -pi -e "s#(agentcontextdistributionprotocol/agentcontextdistributionprotocol/(?:blob|tree|raw)/)$old#\${1}$new#g"
cargo test --locked -p acdp-registry-server --test conformance_gate sibling_repo_links_are_pinnedThe rewrite touches link URLs only: dated records keep the SHA they were written
against, and the "today" value in the Link-convention block of
docs/README.md is prose, so update it by hand. Then
re-check each #anchor against the new revision's headings — the guard reads no
network and cannot see a renumbered section.