Repository navigation
Retention: the locks hold nightly — apply the decided 30-day window to the paused purge-old-images, read production back, and inventory bundles/active builds as consumers #3438
Description
Current delivery scope — 2026-09-09
This remains the end-to-end retention issue. The platform owner's #3842 rule supersedes the historical pin-based prescription below: module builds resolve released platform sets at run time, same-major adopted fallbacks may keep serving, red/cancelled successors must remain invisible, and readiness is announced. Retain unreferenced continuous artifacts for at least 30 days by age, without a build-count quota; supported official releases follow the underlying .NET lifetime, with plugin counterparty support also required.
The remaining work is split into independently verifiable pieces:
- Retention: require a complete and fresh fleet inventory before releasing artifact protection #3858 — complete and fresh fleet inventory, including actual adopted fallback identities. Core fix: retain builds reported as adopted by remote portals #3849 / Plugins#1564 implement the reporting seam; deployment and full fleet coverage still need verification.
- Retention: preserve the full artifact set required by supported releases and advertised fallbacks #3860 — resolve and preserve every artifact needed by supported releases, last-green advertised sets, active builds and consumers.
- Retention: interlock cleanup with protection updates and active publication #3859 — coordinate protection and cleanup so failed collection or concurrent publication cannot expose required artifacts to deletion.
- Verify the 30-day policy on production consumers and the final deployed cleanup task. Core fix: retain bundles by age and released major instead of build count #3843 and fix: enforce a thirty-day assembly-cache retention floor #3846 are merged; that alone is not production-behavior verification.
Keep this parent open until the production task definition and reference-protection behavior have been read back and verified. The split does not authorize destructive cleanup, additional access or an architectural choice without the maintainer's involvement. #3461 remains the separate publisher-versus-publisher overlap issue.
The original incident and previous investigation follow unchanged as historical evidence.
What happened
MeshWeaver.Education main pins the tester image by digest:
mw-plugin-test@sha256:df19f10a…
ACR no longer has it. Every run of that repo's Disposable-mesh gate dies at manifest unknown
before a single content gate executes — including main's own nightly (run 1013). The repo can
merge nothing: Disposable-mesh gate is one of four required contexts in ruleset 19153714
("main pr protection"). Education#250 is the pin bump and is itself blocked on an unrelated wall, so
the repo has been fully wedged.
Root cause
There are two ACR purge tasks on the meshweaver registry, and only one of them learned the
lesson from the last incident.
purge-old-ci-releases (0 4 * * *) — hardened after Memex#122, and it says so in the task
itself:
# memex-website is DELIBERATELY absent from these filters (Memex#122, 2026-08-26).
# … The test for adding any repository here: if this tag were deleted tonight, would anything
# recreate it? The repositories below are continuously republished. A pinned tag is not.
acr purge --filter 'memex-migration:.*' --filter 'memex-bake:.*' --filter 'memex-portal-next:.*' \
--filter 'memex-portal:.*' --filter 'memex-portal-ai:.*' --ago 30d --untaggedpurge-old-images (0 3 * * *) — no comment, no reasoning, and it is the one that bites:
acr purge --filter 'mw-plugin-test:.*' --filter 'memex-migration:.*' --filter 'memex-portal-ai:.*' \
--filter 'memex-portal-next:.*' --filter 'memex-bake:.*' --ago 7d --keep 10 --untagged🚨 Why the existing rule does not cover this — a SECOND failure mode
Memex#122's test is "if this tag were deleted tonight, would anything recreate it?" — and
mw-plugin-test passes it. It is republished many times a day; it currently carries latest,
main, 3.0.0-ci.7903, 8358471, 1f652b7 and several staging-… tags. By that test it is a
perfectly legitimate purge target.
It broke anyway, because the danger is not "nothing rebuilds this repo". It is:
Something PINS a specific digest of it.
The whole fleet pins this image by digest — MW_IMAGE_DIGEST and MW_PORTAL_IMAGE_DIGEST in every
satellite's ci.yml, moved deliberately as one set and left stable for weeks at a time. That is the
build process working as designed (Doc/Architecture/ModuleBuildArchitecture): a pin is supposed to
be stable. --ago 7d --keep 10 guarantees that any pin older than about a week, or displaced by ten
newer builds, refers to bytes that no longer exist.
Republishing frequency does not protect a pinned digest — it is what destroys it, because
--keep 10 is measured in newer builds.
So the retention rule needs a second clause:
- (Memex#122) If nothing would recreate this tag, never age-purge it.
- (this issue) If anything PINS a digest of this repository, the purge must not delete what is
pinned — regardless of age or how many newer builds exist.
Suggested fix
Reconcile retention against the live pin set rather than guessing an age. The pins are readable:
MW_IMAGE_DIGEST / MW_PORTAL_IMAGE_DIGEST in each satellite's .github/workflows/ci.yml
(MeshWeaver.Plugins, .Education, .Reinsurance, .SocialMedia, .Manufacturing) plus whatever core and
Memex pin.
Options, roughly in order of preference:
- Protect pinned digests explicitly — a job that reads every satellite's pins and locks those
manifests (or writes them into a keep-list the purge honours). Self-maintaining; a pin move
releases the old one on the next run. - A guard that reds when a pin no longer resolves in ACR — cheap, and it turns an
entire-repo wedge into one red check naming the repo and the digest. This is worth doing
regardless of which retention fix lands, because it catches the next variant too. - Raising
--ago/--keepis not a fix — it moves the cliff, and a pin left stable for a quarter
walks off the new one just the same.
🚨 Whatever lands must not depend on a human remembering to add a repository to the right one of two
tasks. The two tasks having diverged is itself the defect; consider whether there should be one.
The triage trap, worth keeping
From Memex#122 and it applies here too: the purge run and the breakage are not adjacent in any
log. Pods keep serving from their nodes' image cache long after the delete, so the failure surfaces
whenever the next pull happens — a reschedule, or in this case a fresh CI runner. Do not expect the
ImagePullBackOff / manifest unknown timestamp to be near the purge.
Evidence
az acr task show --registry meshweaver --name purge-old-images— the filter list aboveaz acr task show --registry meshweaver --name purge-old-ci-releases— the hardened oneaz acr repository show-tags --name meshweaver --repository mw-plugin-test --orderby time_desc
→latest,main,1f652b7,3.0.0-ci.7903,8358471,staging-1f652b7-34040888432(+arch variants)- MeshWeaver.Education
main: pinmw-plugin-test@sha256:df19f10a…, run 1013 (nightly) red at
manifest unknown - Prior incident: Memex#122 (2026-08-26) —
memex-websiterepository deleted entirely, public brand
site 503 for ~11 h, nothing alerted
Activity
rbuergi commentedon Sep 6, 2026
Blast radius, measured — three repos went down simultaneously
From meshweaver-bd, who was working the resulting wedge:
That is three satellite repos' CI dead at once from one retention run — and, as recorded above, in
Education's case including
main's own nightly, becauseDisposable-mesh gateis a required contextand it dies at
manifest unknownbefore any content gate executes.This is not a historical problem — it is about to recur, and the fix PR is the victim
Education#250's whole wall-1 fix is a pin bump to
staging-bbcb22f-34015096490-linux-x64, chosendeliberately because its plugins seal is self-consistent (0 declines, against 5 from the previous
candidate). With
purge-old-imagesrunning--ago 7d --keep 10overmw-plugin-test:.*nightly,that new pin walks off the same cliff in about a week — and Education goes red on
manifest unknownagain, which is precisely the failure #250 exists to fix.So the retention policy will undo the fix for the retention policy's last victim. That is the
argument for treating this as more than tidy-up.
Why the priority is higher than "a CI annoyance"
to compare against — the repo loses its control arm at the same moment it loses its gate.
the purge run and the breakage are never adjacent in any log (Memex#122's lesson, and it applies
identically to a CI runner pulling a tester image).
(
Doc/Architecture/ModuleBuildArchitecture, pins move as one set, deliberately and rarely). Theretention window and the pin lifetime are in direct contradiction, and nothing reconciles them.
Minimum viable mitigation, if the full fix is not immediate
The cheap half is the guard, not the policy: a check that reds when a repo's
MW_IMAGE_DIGEST/MW_PORTAL_IMAGE_DIGESTno longer resolves in ACR. It does not stop the deletion, but it converts"an entire repo's CI is inexplicably dead, including its nightly" into one named red saying which
digest is gone — which is the difference between minutes and the several hours this cost today.
rbuergi commentedon Sep 6, 2026
The guard half is up: #3462 — and the blast radius is wider than the tester
Two of Education's pins beyond
MW_TEST_DIGESTare also gone. Measured against the liveregistry, 2026-09-06:
All three repositories are in
purge-old-images's filter list, so one task took the whole set. Thepin bump has to move all three, not one.
Fleet sweep, with the denominator printed — 8 scanned · 6 pin · 2 do not (core, Memex) · 12 pins
· 9 resolve, 3 gone, 0 indeterminate, 0 malformed.
🚨 #3454's "SocialMedia, Manufacturing, Crm — 0 matches" is a zero with two causes. All three DO
pin: Manufacturing as
MW_IMAGE_DIGEST, SocialMedia and Crm inside a$GITHUB_OUTPUTstep output(
echo "image-digest=sha256:…"), which no scan ofenv:blocks can see. With Education'sMW_PORTAL_DIGEST/MW_MIGRATION_DIGEST/MW_TEST_DIGEST, a check keyed on the two commonvariable names reports four of the six pinning repositories clean while verifying none of their
pins. The guard extracts by shape, not by name, and prints both halves of the denominator so a
zero cannot hide its cause.
The retention half: cloud-only, so there is no IaC PR to open
Established three ways rather than assumed:
az acr task showreturns anEncodedTaskwithcontextPath: nullfor both tasks — the YAMLis base64 inside Azure, with no source repository.
acr purgefinds only two MeshWeaver doc pages.deploy/aks/infra/main.biceptreatsmeshweaver.azurecr.ioas an existingsharedAcrLoginServerand only creates a registry when that is empty.So changing retention is a hand-applied change to shared registry infrastructure. Two findings for
whoever applies it, both measured:
acr purgeskips locked manifests by default.--include-lockedis the documented opt-in todelete them, and neither task passes it — so
az acr repository update --image <repo>@<digest> --delete-enabled falseis a hard protectionagainst this exact configuration, not a hint. That makes "protect the live pin set" implementable
without changing what the purge deletes in general.
purge-old-ci-releases(04:00,--ago 30d) namesmemex-migration,memex-bake,memex-portal-next,memex-portal,memex-portal-ai;purge-old-images(03:00,--ago 7d --keep 10) already swept all of those anhour earlier except
memex-portal. The only repository unique to the aggressive task ismw-plugin-test— the one that broke. One task, one filter list, one window removes the"which of two lists?" question by construction.
The exact
azcommands, the merged task YAML with the reasoning kept in-task, and the argument forwhy raising
--ago/--keepis not a fix are in the doc page #3462 adds:Doc/Architecture/PinnedImageRetention.Leaving this issue open: the guard is the cheap half, the retention change is outstanding and is
the maintainer's to apply.
docs(#3438): name where the pin extractor stops, so an unseen shape i…
fix(#3438): the sweep's own fleet list could lose a page, and "pins" …
docs(#3438): merging the two purge tasks must not tighten memex-porta…
docs(#3438): two purge steps share one task timeout budget, so say so
fix(#3438): an ABSENT repository read as one with no pins, and the sw…
rbuergi commentedon Sep 6, 2026
Guard half MERGED — and its first run on
mainreds naming this issue's three digests#3462 merged as
9dfce19c0, verified onorigin/main(the merge queue does not fireCloseslinks). The
pushtrigger fired the guard's first real run immediately, against the live registry:run 34055176037 — and it is
RED, which is the guard working, not a defect.
The denominator, as printed in CI:
Both arms proven on the real fleet in one run: the dead arm names Education's three with repository,
declaration and digest; the live arm resolves 12 declarations including
mw-plugin-test@sha256:642f686f…(3.0.0-ci.7917), the wave's new pin. 34 repositories scanned,not a committed six — the fleet list comes from the App installation, which is org-wide.
🚨 Education's bump must move all three, not just the tester #3438's title names.
Four defects were found and fixed by falsifying the guard rather than reading it
Each is the same shape as the incident: not checked spelled identically to clean.
.github/workflowsanswers 404 both for "no workflows" and for "no such repository / no access".Measured:
--repos …,Systemorph/ThisRepoDoesNotExist12345printed "0 workflow file(s), no digestpin declared" and exited 0. The repository is now probed before any absence is believed —
absent ⇒ UNREADABLE and red;
Systemorph/iac(real, no workflows) still a measured zero.gh api --paginateoutput was re-parsed bysplitting text on braces, swallowing the decode error. Now
--jq '.repositories[].full_name'.and labelled separately (15 vs 7 above — the fleet pins few manifests from many places).
memex-portal. See below.The retention half — still OPEN, still the maintainer's to apply
Cloud-only, re-established independently: both tasks are
EncodedTaskwithcontextPath: null;org code search for
purge-old-imagesreturns 0 hits and"acr purge"only two doc pages here;Systemorph/iacholds just a README;deploy/aks/infra/main.biceptreatsmeshweaver.azurecr.ioas an existing
sharedAcrLoginServerand builds a registry only when that is empty. There was noIaC PR to open.
🚨 The correction that matters before anyone applies it. The two filter lists differ by exactly
two repositories and they are not symmetric:
mw-plugin-testis unique to the 7-day task (theone that broke), but
memex-portalis unique to the 30-day task and is the production portalimage. The obvious merge — union the filters, keep the stricter window — moves
memex-portalto7d --keep 10with nobody deciding to. And this guard cannot cover it:memex-portalis pinnedon the other axis, committed image TAGS in the deployment overlays (Memex#141), which a scan of
.github/workflowsnever enumerates. That is exactly how Memex#122's victim was pinned.So the proposed task in
Doc/Architecture/PinnedImageRetentionis behaviour-preserving: onetask, two steps, each keeping the window that repository already has. It removes the second
task, not the second window. The lock set is correspondingly the union of both pin axes.
Also confirmed against upstream
Azure/acr-cli:acr purgeskips locked manifests by defaultand
--include-lockedis the opt-in to delete them — neither task passes it, soaz acr repository update --delete-enabled falseis a hard protection under this exactconfiguration. And raising
--ago/--keepis explicitly not a fix: it moves the cliff.Leaving this issue open. The guard is in; the retention change is a hand-applied change to
shared registry infrastructure and is not mine to make. Exact commands, the merged task YAML with
its reasoning kept in-task, and the timeout caveat (two steps share one 3600 s task budget) are all
in
Doc/Architecture/PinnedImageRetention.fix(#3454): the fleet set-consistency arm must not sit dead behind th…
rbuergi commentedon Sep 7, 2026
First real run: 34068789826 — RED, naming this issue's three digests
The merge's
pushfired the job immediately, in report-only mode as designed. Every arm behavedas specified, against the live registry:
Assert the inputs exist→ success (every secret present, App token minted, fleet enumerated).The purge still skips locked manifests→ success (2 tasks, 3 purge steps, none passing--include-locked).Lock every manifest the fleet pins→ failure, on exactly three lines:That is the job working, not a defect — it is this issue, and it clears when Education's bump moves
all three. It will red daily alongside the 05:20 guard on the same fact, by construction.
The OIDC credential's READ half is confirmed working — the control probe enumerated 2,846
manifests. Only the WRITE is untested, and it is untested because this run was report-only. The
first
applyrun (01:00 UTC, or aworkflow_dispatch) is the decisive measurement of whether theContainer Registry Repository Writergrant is needed; on the role evidence above it is.Note the numbers are live: 2,831 → 2,837 → 2,846 manifests across three readings tonight. Every
reading is a snapshot of a moving registry, which is exactly why a hand-maintained keep-list cannot
be right for long.
rbuergi commentedon Sep 7, 2026
🚨 The blast radius is wider than a pinned digest — a MOVING tag is outside #3507's model entirely
This issue documents a pinned digest being purged (
mw-plugin-test@sha256:df19f10a…, the Education wedge). Tonight produced a second shape the fix does not cover, and it is the more exposed one.What happened, 2026-09-07 03:00Z
The
purge-old-imagestask removedmemex-portal-ai:latest, and every MeshWeaver.Plugins CI run began failing at preflight:Attribution is not inferred — the same head
e8f315bf9ran green at ~01:0xZ (run 34071193971) and red at 03:26Z (run 34079642842). Same commit, same workflow, opposite outcome, with the 03:00Z purge in between.The state is worse than one tag — four of five
:latestmw-plugin-testmemex-portal-aimemex-migrationmemex-portal-nextmemex-bake🚨 The survivor is the anomaly, not the casualties.
mw-plugin-test:latestsurvives because that repository republishes rarely enough that ten newer builds have not accumulated since July. It is one busy week from the same fate — and when it goes, both halves of the preflight's pair vanish and the failure changes from "the promoted set is incomplete" to no reference set at all.Why #3507's job does not cover this
lock-pinned-digests.pyderives its set from literal pins —MW_IMAGE_DIGEST,MW_PORTAL_IMAGE_DIGEST,platform-image-digest:, deployment overlays. A moving tag is named by nobody, so it appears in no extraction axis and is locked by nothing. The job is correct about what it models; the model has a hole shaped exactly likelatest.And a moving tag is structurally more exposed than a pinned one:
--keep 10counts newer builds, so every republish spends its own budget.memex-portal-ainow holds 788 manifests. Tonight's CD burst — sets 7962, 7964, 7967, 7969 plus three staging tags each — consumed the keep window in a single evening. 🚨 The mechanism gets worse precisely when the fleet is most active, which is the opposite of when you want retention to bite.Ownership of the cause, stated plainly
The two-step
purge-old-imagestask with--ago 7d --keep 10overmemex-portal-ai:.*was applied earlier the same night, by me, as this issue's remedy. Its own header predicted this outcome — "--keep 10counts NEWER BUILDS, so frequent republishing is what DESTROYS a pin rather than protecting it" — and I protected pinned digests with--delete-enabled falsewhile leaving moving tags outside the lock model. The eviction is the first bill for that gap, not a surprise.What is deliberately NOT done
🚨 No retag. Restoring
memex-portal-ai:latestto a September image would pair it with a July tester in whatever resolves that "promoted set" — the image-pair skew class that 503'd every signed-in request on 2026-09-03. Incomplete fails loudly at a preflight; inconsistent fails at a signed-in request. It is a fleet-wide reference-set decision and it belongs to the maintainer, with two parts:latestmeant to exist on these repos at all? In practice it exists on one of five, by accident of quietness. If the preflight requires it, the preflight asserts a convention nothing maintains.🚨 Do not raise
--ago/--keep. The task's own header says it moves the cliff rather than removing it. The durable fix for a moving tag is the same as for a pinned one —--delete-enabled false, whichacr purgeskips and neither step passes today — plus teaching #3507's extractor that a tag a lane resolves is as much a pin as a digest a file names.rbuergi commentedon Sep 7, 2026
The purge took
memex-portal-ai:latestat 03:00Z and blocked every Plugins CI run — fixed by leavinglatestbehindRecording this here because the remedy was a repo variable, which lives outside git and is otherwise invisible to anyone reading the workflow.
What broke
Attribution is not inferred: the same head
e8f315bf9ran green at ~01:0xZ and failed at 03:26Z. Same commit, same workflow, opposite outcome, with the purge at 03:00Z in between.The blast radius is wider than this issue documents
This issue records the tester digest (Manufacturing/SocialMedia, 2026-09-05T15:29Z). Measured across all five filtered repositories:
🚨 Four of five. And the survivor is not safe — it survives only because that repository republishes rarely enough that ten newer builds have not accumulated since July. It is one busy week from the same cliff, and when it goes, both halves of the preflight's pair vanish and the failure changes from "incomplete set" to "no reference set at all".
🚨 A MOVING tag is more exposed than a pinned one, which is the opposite of the intuition the lock work is built on: every republish spends its keep budget. Tonight's four CD sets plus three staging tags each consumed
--keep 10in a single evening. #3507's job locks pinned digests only; a movinglatestis outside its model entirely.The fix, and why it is not a retag
The preflight derives the portal image from
MW_TEST_IMAGE's tag — "Same registry, same tag, sibling repository". So the two must be one matched set. Retaggingmemex-portal-ai:latestto a September build beside a July tester would have paired two halves from different builds — the image-pair skew class that 503'd every signed-in request on 2026-09-03. Incomplete fails loudly at a preflight; inconsistent fails at a user's request.Instead,
MW_TEST_IMAGEnow names a matched, promoted, verified-sealed set:Both halves resolve (2 platforms each,
sha256:c0940ed2…/sha256:5899d18d…), and:lateststill does not — the negative control.🚨 7969 was chosen by checking the seal, not by taking the newest.
#7967is newer than 7964 and failed its seal (failure,skipped,successacross promote/verify/seal), so "latest version tag" would have been the wrong pick. 7969: runsuccess, promote/verify/seal allsuccess.What this does not fix
latestis still absent on four repositories and still unmaintained by CD. Whether it should exist at all is a maintainer call — in practice it exists on one of five.--ago/--keep. The task's own header says it moves the cliff; the durable fix is--delete-enabled false, whichacr purgeskips and neither step passes today.rbuergi commentedon Sep 7, 2026
Both questions about the lock job's red are settled by measurement — and neither wanted a change. What DID want one is the read-back (PR #3563)
1. The red was CORRECT and it has already cleared itself
Education's
mainsat at53f0a65df(2026-09-04T12:12:07Z) untild622e7dc9merged PR #274 at2026-09-07T05:52:00Z —
d622e7dc9's first parent is53f0a65df, and that commit'sci.yml:45/46/64carries exactly the three purged digests:So at 00:07Z (push) and 01:18Z (schedule) the default branch really did declare three manifests that
no longer exist. I ran the extractor live against
Systemorph/MeshWeaver.Educationat ~08:20Z todayand it now returns the three you verified as locked:
🚨 The trap that made this look like a scanner bug: the bump commit
4c471c2f3is dated2026-09-07T00:31:19Z, before the 01:18Z scheduled run — but that is its authoring date on the
branch
fix/246-platform-pin, not the moment it reachedmain.commits?path=…walks all parentsand orders by date, so it interleaves branch commits with trunk ones and reads as a trunk history that
already had the fix. The merge commit's first parent is what actually answers.
2. "Read the default branch rather than every branch" — it ALREADY does, and changing it would be harmful
check-pin-set-consistency.py:scan_remotecallsrepos/{repo}/contents/.github/workflowswith no?ref=, and the GitHub contents API defaults to the repository's default branch. So the staledependabot branches on Education #277/#278/#279 were never in the picture — no branch but
mainhasever been scanned.
And it should stay that way. Reading every branch would be wrong in both directions: red forever
on a commit nobody will merge, and — the worse one — a manifest that nothing live pins entering the
lock set on the strength of an abandoned branch, i.e. the derivation protecting bytes for a reason
that no longer exists. The lock set is only sound because its input is what the fleet actually runs.
3. "It should name the stale pin and which repo declares it, not just die" — it already does
Verbatim from run 34072578951:
Repository, file, line, variable, digest, consequence and remedy. It also prints
UNPROTECTABLE …in the report body with every declaration site. Locks are applied even on adegraded run, so those three never blocked the protection of anything else.
I considered splitting the exit into "a pin is stale" vs "this job could not check", so a chronic
stale pin cannot bury a new blocker. I did not do it: the script's 28-sabotage self-test asserts
against
plan.blockersin seven arms, it runs on every PR in the fleet, and the change would buy areport distinction the log already makes while risking a red across every C#-touching PR. If a pin
goes stale for weeks rather than days, that trade is worth revisiting — recording the reasoning rather
than the conclusion.
What DID want a change, and it is load-bearing right now
The job does exactly one thing: set
deleteEnabled: false. It believedaz's exit code that ithad:
An exit code is a statement about the request. A write that returns 0 without the attribute moving
leaves the job printing
locked …per manifest andProtected N manifest(s)at the end, overmanifests the 03:00 purge can still delete — and the workflow header already claimed it "fails closed
on … a lock that did not take". Nothing enforced it. That is the same failure this whole job exists to
remove, pointed at itself.
PR #3563 makes
apply_locksreaddeleteEnabledback and requirefalse;trueis red, and anINDETERMINATE read-back is not a confirmation. Two
--self-testarms drive both cases and werefalsified by deleting the read-back (
5 lock(s) were COUNTED although none took).🚨 This matters now because the WRITE half has never actually run — both runs so far were
report-only or blocked. The first
applyrun is the decisive measurement of whether locking works atall, and it must not be able to report success without having achieved it.
Still yours, unchanged and listed rather than invented
az acr repository updateneedsMicrosoft.ContainerRegistry/registries/repositories/metadata/write— inContainer Registry Repository Writer(and Contributor/Owner), in neitherAcrPullnorAcrPush. The registry's three service principals hold onlyAcrPull,AcrPushandContainer Registry Data Importer and Data Reader(metadata/readonly). Until it is granted thelock step reds once, naming the exact
az role assignment create.latest-tag retention policy. Four of five filtered repositories have no:latest; thesurvivor (
mw-plugin-test, 2026-07-20) survives only by being quiet. Two parts, both yours:is
latestmeant to exist on these repositories at all — in practice it exists on one of five,by accident — and if so, all five move together to one promoted set, never one tag to a
September image beside a July one. A moving tag is also outside Every pinned manifest is locked nightly, so retention cannot delete it (#3438 lock half) #3507's lock model entirely, since
no file names it.
🚨 Not proposed, and it would not have worked:
--delete-enabled falseis not anacr purgeflag. It is a manifest attribute set by
az acr repository update, which is exactly what #3507's jobalready does — and the purge tasks' own header says so: "PROTECTION IS THE MANIFEST LOCK, not this
policy …
acr purgeskips locked manifests by default and neither step passes--include-locked."Measured 2026-09-07: the extractor run live against Education, the commit ancestry via
compareand the merge commit's first parent, and the run log of 34072578951. No registry mutation performed.rbuergi commentedon Sep 7, 2026
STAYING OPEN — and Memex#219 does not cover either remaining half
Checked 2026-09-07T12:25Z during a sweep of merged-but-open issues. The hypothesis worth killing
here is "the code half merged and the retention half is tracked on Memex#219, so this is closable".
It is not, and this note exists so nobody closes it on that reading.
The code half is genuinely merged. #3563, merge commit
b37d2079donorigin/main(
fbf9e9be1) —apply_locksreadsdeleteEnabledback and requiresfalse;trueis red, and anindeterminate read-back is not a confirmation.
What Memex#219 actually says (read in full today,
state = open, created 2026-09-07T07:35:07Z):it asks for "a retention policy that cannot delete a tag any committed overlay or any live workload
references — or, if that cannot be expressed, keeping versioned
memex-*tags longer than thecurrent window." That is the versioned-pin problem, split out of Memex#141.
Neither of this issue's two remaining halves appears in it, nor anywhere else in Memex.
Denominator: all 13 open issues in
Systemorph/Memex(#116, #119, #132, #148, #152, #165, #172,#188, #204, #219, #220, #221, #228), titles and bodies searched for
Repository Writer,metadata/write,deleteEnabled,latest tag,:latest— zero hits.So both remain untracked outside this issue:
az acr repository updateneedsMicrosoft.ContainerRegistry/registries/repositories/metadata/write, which lives inContainer Registry Repository Writer(and Contributor/Owner) and in neitherAcrPullnorAcrPush. This is load-bearing right now: fix(#3438): the pinned-manifest lock reads the attribute back — an exit code is not a locked manifest #3563 just made the lock job refuse to report a lockit did not achieve, and the write half has never run, so the first
applyrun reds on the missinggrant.
latest-tag retention question. Four of five filtered repositories have no:latest; thesurvivor (
mw-plugin-test, 2026-07-20) survives by being quiet. Two decisions, both themaintainer's — is
latestmeant to exist on these repositories at all, and if so do all five movetogether to one promoted set. A moving tag is outside Every pinned manifest is locked nightly, so retention cannot delete it (#3438 lock half) #3507's lock model entirely, since no file
names it.
If those two are wanted on Memex rather than here, they need their own issue there — folding
them into #219 would attach a Container-Registry-RBAC action and a tag-hygiene decision to a
retention-window cost trade-off, which is how one of them gets lost.
Measured 2026-09-07T12:25Z: merge commit via
git log origin/main --merges --greponfbf9e9be1; Memex#219 and the 13-issue open set read over REST(
repos/Systemorph/Memex/issues?state=open&per_page=100, page complete — no second page).rbuergi commentedon Sep 8, 2026
The WRITE half works — first successful
applyrun, and the two-task question is settledTwo things this issue's thread predicted rather than measured are now measured, and both change what
is written above.
1. ✅ The lock actually locks. The RBAC grant landed and the first apply run succeeded
Every earlier run was report-only or blocked, and the page said so: "the write half has never
actually run". The scheduled run of 2026-09-08T01:14Z
(34176008757) is the first
applyrun in the job's history to succeed, and it wrote locks:0 that could NOT be protectedis the first end-to-end measurement of the mechanism rather than ofits plan — and it is only evidence because #3563 made
apply_locksreaddeleteEnabledback. Hadit still believed exit codes, that line would have been indistinguishable from 18 manifests the
purge can still delete.
Education's three unprotectable digests are gone from the report: its bump moved all three, to
memex-portal-ai@47dc1750…,memex-migration@24ea2fc6…,mw-plugin-test@4cef7b67….🚨
…locked and pinned by NOTHING 16is the standing debt of a lock-only job, and it grows everytime a pin moves. The release arm that would clear it stays disabled on purpose.
2. The second purge task — settled from the task definitions, not from a summary
az acr task showon both, decodingstep.encodedTaskContent, 2026-09-08:statusschedulepurge-old-images0 3 * * *--ago 7d --keep 10 --untagged;memex-portalat--ago 30d --untaggedpurge-old-ci-releases0 4 * * *So this issue's headline is settled, and the settlement is that there is no longer a second task to
teach. The divergence itself was the defect:
mw-plugin-testappeared in the aggressive list andin no other, and whether a new repository landed under a 7-day or a 30-day window depended on
which of two independently-edited lists someone happened to open. Both carry
lastModifiedAt2026-09-06T21:51Z, ninety seconds apart — the edit that collapsed them.
🚨 The committed record is not the registry.
.github/acr-retention/*.yamlis a record; nothingdeploys from it, so "the record says Disabled" is not evidence the task is. The reading above was
taken by hand, read-only, against Azure.
The denominator, hand-counted — pins EXAMINED, not only pins at risk
A plain
sha256:grep over every workflow file in all 34 org repositories, deliberately notthe shipped extractor, so the two are an independent cross-check:
.github/workflowsdirectorysha256:-shaped tokens foundTwo things only a hand count shows. Eleven of thirty-one
sha256:tokens are comments — severalof them narrating this incident inside the very files it broke — so any "count the digests"
instrument that does not exclude them over-reports by more than a third. And 20 sites collapse to
15 declarations in the shipped report, which dedupes on
(name, digest)because both Plugins andCrm write the same
platform-image-digest:value at two lane calls. 9 distinct manifests is whatretention actually has to keep alive, and it is the number the two counts agree on.
The two unlocked ones are not a defect: tonight's 01:00 lock precedes tomorrow's 03:00 purge.
🚨 The gap that was still open, and #3736 closes it
Existence is a LAGGING indicator. By the time a pin reads
GONE, the repository is alreadywedged. Nothing was a leading indicator — and nothing asserted that the protection was still
protecting, from a process that is not the lock job itself. Each of these left a green wall until a
satellite died at
manifest unknowndays later:lock-pinned-digests.ymldeleted, itsschedule:removed, the
metadata/writegrant revoked, a manifest unlocked by hand, the cron moved past thepurge. That is this issue's own failure mode one level up.
#3736 makes
check-pinned-digests.pyreadchangeableAttributes.deleteEnabledoff every pin itresolves — the same
azcall,-o jsoninstead of-o none, no extra traffic — and classifyPROTECTED/UNPROTECTED/PENDING-LOCK/INDETERMINATE. Only the registry sayingdeleteEnabled: falseearnsPROTECTED; "the field was not in the response" is INDETERMINATE andred, because a protection that cannot be read has not been shown to be working.
It also asserts, credential-free and therefore on every pull request rather than only at 05:20,
that
lock-pinned-digests.ymlstill exists with aschedule:and that its cron fires beforeevery enabled purge step recorded in
.github/acr-retention/tasks.json.acr purgeskips lockedmanifests and that workflow is what locks them, so the order of those two clocks IS the
protection; move the lock to 04:00 and every pin moved since the previous night faces the purge
unprotected, with both jobs still reporting success about their own work.
Falsified rather than asserted, each watched to go red: the lock cron moved to
0 4 * * *; the lockworkflow deleted; two sabotaged schedules driven through the same comparison inside
--self-test;and the classifier made to answer
PROTECTEDfor everything (five arms red, includingSABOTAGE UNDETECTED: an UNLOCKED pinned manifest classified as PROTECTED). The classifier is apure function precisely so every branch can be driven — the
UNPROTECTEDbranch cannot be reachedlive without unlocking a production manifest, which a test may not do.
PENDING-LOCKis an exemption and it is counted: a pin that moved after the last scheduled lockhas had no opportunity to be protected, so failing it would red on the fleet's ordinary working day.
The proxy softens, never false-alarms, and the exempted count is its own reported line so the arm
cannot go vacuous unnoticed. A missing lock schedule exempts nothing.
What stays open, and it is not code
The residual exposure is structural and is named rather than papered over. Protection is
derived on a daily cadence and the purge does not wait for it: the lock is a GitHub Actions cron,
the purge an ACR timer task, with no interlock between the two systems. So a pin landing in the
01:00–03:00 window meets that night's purge unprotected, and if the lock job does not run at all
(an Actions outage, a failed OIDC login, a revoked grant) the purge still runs at 03:00 regardless.
Neither is a widened window away from being fixed — raising
--ago/--keepmoves the cliff. Bothclose only by making the pin move and its lock one act, which means a cross-repo dispatch from
each satellite and is not built. Until it is, #3736's protection arm is what makes that exposure
visible instead of silent.
And the
latest-tag question is unchanged and still the maintainer's — four of five filteredrepositories have no
:latest; a moving tag is named by no file, so it is outside #3507's lock modelentirely.
All of it is committed to
Doc/Architecture/PinnedImageRetention, which is the durable form; thiscomment is a pointer to it.
Measured 2026-09-08, read-only throughout: no purge run, no dry run, and no registry mutation of
any kind.
az acr task show×2,az acr repository showper distinct pinned manifest, the run logof 34176008757, and the org repository list plus every workflow file over REST.
31 remaining items
rbuergi commentedon Oct 10, 2026
Governed path merged; production is unchanged. Maintainer decision (2026-10-09): "#3438 registry retention: apply the decided 30-day window + re-enable purge-old-images through a GOVERNED path (InstanceAction on control; if none exists for registry tasks, build that action kind — never a hand az call), then read production back."
Merged:
memex-portal-aifrom their deployment record, which the lane never searched; it now reads each installation's record.registry-retentionandfleet-registry-bounded.RegistryRetention(Hosting 1.62). A dry run is a read-only verify that needs no approval. A real run applies the record, and needs the deployment id typed back plus one approval.Container Registry Tasks Contributorgrant for hosting-operator ininfra/modules/registry-tasks-grant.bicep.Maintainer acts, in order (no agent approves a governed action):
RegistryRetentionmust appear as an action kind there.fb382eab-e894-4461-af04-94435c366c3f(Container Registry Tasks Contributor) to hosting-operator. The exact command is in Memexdocs/estate-least-privilege.md. Until then, that one step of InfraDeploydeployfails by name.RegistryRetentionwithdryRun: true. It reads back the live ACR tasks and the storage lifecycle policy, and needs no approval.RegistryRetentionfor real (confirmation plus approval). This installs the 30-day window onpurge-old-images, which stays disabled.pauseand setspurge-old-imagesto Enabled, then apply it again with approval, then read production back.This issue stays open until step 6 has been read back from production.
rbuergi commentedon Oct 11, 2026
(re-posted 2026-10-11; originally by @rbuergi on 2026-09-07)
Lock half MERGED — and one production action stands between it and actually protecting anything
PR #3507 merged as
deccceceef607ac9038c08a3a80cfc7d155b9482, verified onorigin/main(the merge queue does not fireCloseslinks). Requirement 1 — protect pinned digests explicitly, without depending on a human
remembering — is implemented, falsified and scheduled.
.github/workflows/lock-pinned-digests.ymlruns at 01:00 UTC, two hours before the 03:00 purge,derives the fleet's live pin set and locks every manifest it names.
acr purgeskips lockedmanifests and neither step of
purge-old-imagespasses--include-locked, sodeleteEnabled: falseis a hard protection against this exact configuration.
🚨 LEAVING THIS OPEN. One grant is missing, and it is only the maintainer's to make
Locking is a WRITE.
az acr repository updateneeds the data actionMicrosoft.ContainerRegistry/registries/repositories/metadata/write. Measured 2026-09-07:metadata/write?AcrPullAcrPush(pull/read+push/write, no dataActions)Container Registry Data Importer and Data Readermetadata/**read**Container Registry Repository WriterThe
meshweaverregistry's three service principals hold only the first three, measured with--include-inherited. So on the evidence the nightly lock will be refused until this lands:The job reds once on refusal, naming that command, rather than repeating one error per manifest.
It is a provisioning task, not a workflow defect — and it is the reason this issue stays open.
What a real run would do, live, report-only, 2026-09-07
34 repositories, 2m19s, exit 1 (correctly — see below).
Seven manifests are pinned and unprotected right now, four of them in repositories the 7-day step
sweeps:
memex-portal-ai+memex-migrationat3.0.0-ci.7926(what both live deployments run),memex-portal-next:3.0.0-next.43, and the client-b overlay's3.0.0-rc9.ci.7601pair.The three unprotectable ones are this issue — Education's
memex-portal-ai@dab2a7b3…,memex-migration@d81df6cc…,mw-plugin-test@df19f10a…, already deleted. Education's bump must moveall three.
🚨 The unlock half is DESIGNED and DOCUMENTED but SHIPS DISABLED, and the first live run showed why
Of the five manifests locked by hand on 2026-09-06, the job would have protected two and listed
the other three as release candidates — the
staging-bbcb22f-…set that Education's pendingpin bump will name, locked pre-emptively. No derivation from the live pin set can see a pin
nobody has committed. An enabled release arm would have unlocked the fix for this issue's own
victim, on its first run, and
--ago 7dwould have finished the job.So: locks are applied even on a degraded run (a lock cannot destroy anything); releases are computed,
printed and never acted on unless a maintainer sets
MW_ACR_RELEASE_UNPINNED=trueand the runcarries no blocker. A pre-emptive lock is a legitimate reason for a lock the job cannot derive.
Two things this issue's premise got wrong
memex-portalis not pinned by any deployment overlay. The merged task comment and the docjustified the two-step split by saying it is (Memex#141). The overlays pin
memex-portal-ai,memex-migration,memex-portal-nextandhosting-operator; an org-wide code search formeshweaver.azurecr.io/memex-portal:returns zero hits and the repository's newest tag is3.0.0-rc13. The split is still right on plainer grounds — tightening a low-churn productionimage repository is a separate decision with its own evidence — but the stated reason is false,
and a false reason is how a future merge talks itself into the union. Corrected in the doc.
memex-portal-shaped: 11 sites over 7 distinct manifestsincluding
hosting-operatorandwhisper-swiss-german, and one of its two shapes (helm's splitrepository:/tag:) is invisible to a single-line scan — the same "not seen reads as not pinned"confusion, one layer down.
One thing to check before the next promotion
The five hand locks set both
deleteEnabled: falseandwriteEnabled: false. Only the firstis what
acr purgeskips on; the second additionally refuses a manifest PUT — andrelease.ymlpromotes by retagging (
docker buildx imagetools create --tag $ACR/$repo:$VERSION $ACR/$repo:$SHORT),which is a PUT of an already-present digest under a new tag. Two of the five
(
mw-plugin-test@642f686f…,memex-portal-ai@31aab07d…) are the3.0.0-ci.7917wave — the set a3.0.0promotion would retag. Reasoned from the mechanism, not measured (confirming needs anactual retag).
--write-enabled trueundoes it and leaves the purge protection untouched. The newjob locks delete only.
Also landed
The purge tasks are cloud-only (
EncodedTask,contextPath: null, zero org-wide code-searchhits), so
.github/acr-retention/now records both decoded definitions byte for byte plus theirschedule/status/timeout, with
.github/scripts/acr-retention-tasks.shshow/record/verify/applyrelating the record to the live registry. A credential-free gate on every PR refuses a purge step
that gained
--include-locked— which would turn every lock into decoration while the job keptreporting success.
28 sabotages were run against the three checks and each was watched to go RED. Two vacuous
assertions fell out of that and were fixed: the release arms asserted "nothing was released" against
a fixture with nothing to release (so deleting the guard left the suite green), and the retention
verifier's
--include-lockedgrep fired on the task's own comment saying it does not pass it.Doc:
Doc/Architecture/PinnedImageRetention.rbuergi commentedon Oct 11, 2026
(re-posted 2026-10-11; originally by @rbuergi on 2026-09-12)
Two of the three split issues are CLOSED and folded back here — #4081 landed the mechanism
#3858 and #3860 are closed; #3859 stays open, narrowed to the one thing that cannot be built in
this repository (the destructive schedule itself — see below). The four described one mechanism,
not four pieces of work: a cleanup whose
protection set is derived from a stale or incomplete source deletes something that is still in use.
The 2026-09-09 split called them "independently verifiable", and building them showed they are not
independently deliverable — a fresh, complete inventory is only meaningful once something decides
what INCOMPLETE means, and nothing can be interlocked against a decision with no completeness
verdict. So they were built as one change set, with the detail on each thread; this issue stays open
and is once again the single home for what is left.
What #4081 landed
own unauthenticated
/api/version, what it is RUNNING; the closure of image sets built from thatcommit is protected in every repository its overlay pins. 🚨 The bare short-sha tag is a moving
tag (core
4c99ec26produced two image sets ninety minutes apart and4c99ec2followed the newerone), so the axis protects the closure and says so rather than guessing.
deleteEnabled, and acr-clideletes a tag on the tag's. Measured:
memex-portal-ai:3.0.0-ci.8372— what both productionoverlays pin — read
trueover a manifest readingfalse, and 0 of that repository's 1,396tags were locked. The bytes were protected and the reference was not.
continuecomes before the walk that ignore-lists an index's children, so a locked index is never walked
and its untagged children are collected out from under it. The corollary is worse: an unlocked,
tagged index is walked — so protecting the index alone made its children less safe.
answered / not / declared non-live, manifests, tag references, and the enabled purge windows it is
racing. An installation that did not answer is named, makes the run INCOMPLETE and refuses the
unlock arm.
DeploymentPinnedReferences.Resolvetakes theHosting/Deploymentrecords as the expected-consumer denominator; missing, unreadable or stale(> 24 h) refuses the pass naming the instance.
reddened this lane on 2026-09-12, two hours before the purge, on a premise No platform pins in module repos — build against the released platform, and be TOLERANT of drift (same major serves) #3842 had expired.
What is left here, and it is deliberately not code
at 03:00, and nothing makes the second wait for the first. Tracked there rather than here,
because it has its own acceptance criteria. Nothing destructive was run for #3438: retention protects what is CONSUMED, and refuses when it cannot see it all #4081.
purge-old-imagescarriesmemex-portal-aion its 7-day step — the image both productionportals run — while the 30-day step covers only the retired
memex-portalrepository.:latestquestion. A moving tag is named by no file and is outside this model.Container Registry Repository Writergrant, which is what lets the lane write at all.named that are still not inventoried.
running control instance, not a code change.
client-bis an expected consumer that has never reported. Measured on the control instance2026-09-12: three
Hosting/Deploymentrecords, allActive, none declaring retirement — andclient-bhas never been installed. So the prebuilt-bundle pass refuses there until one field iswritten (
retired/retiredAtwith a reason, in the deployments repo) orclient-bisinstalled. That refusal IS the mechanism working — "is
client-ba consumer?" is the questionretention must answer before it deletes — and it costs nothing today, because the chart
disarms bundle deletion by default. The registry lane already carries the same declaration in
.github/acr-retention/instances.json; the two stores do not infer each other's answer.now, and the platform manifests under every locked index are unlocked. Tonight's scheduled run
locks them if it is allowed to; no hand command was issued.
Design:
Doc/Architecture/ArtifactRetentionInterlock.#4081 merged as
4764a6072e9aae9622d50413d9df3d1bdced5bfe.rbuergi commentedon Oct 11, 2026
(re-posted 2026-10-11; originally by @rbuergi on 2026-09-13)
The 7-day step is closed, and it was DECIDED rather than a decision — #4213
This issue's open item "
purge-old-imagescarriesmemex-portal-aion its 7-day step" has beencarried on the thread and in
Doc/Architecture/ArtifactRetentionInterlockas "whether that is theintended policy is a decision, not a defect". It is not a decision. It was already decided, and
the registry was the one store nobody held to it.
#3842's rule, quoted verbatim in this issue's own body and in #3859's acceptance list:
Three stores implement it. Two were clamped to it in code and the third was asserted by nothing:
PrebuiltBundleRetention.cs:641-642—MinimumAge < 30 days ⇒ 30 days, andKeepNewestPerSourcedocumented as no longer driving deletion (#3843)AssemblyCacheRetention.cs:331-332— the same clamp (#3846)check_retention_recordasserted only that no step passes--include-locked#3843's own body records the gap in as many words: "The ACR task record and live cloud cleanup are
unchanged."
The negative control
The new gate, against the record as it stood on
origin/mainthis morning:After the record states the decided window:
EXIT=0, still3 purge step(s) inspected, and thepre-existing 30-day step passes unchanged — so the gate is not simply reddening everything. Both new
assertions were then falsified: stubbing
check_window_policyreds four ARM 31 lines including"the exact step this repository carried until 2026-09-13 … passed the window policy"; stubbing
check_pause_coherencereds three ARM 31b lines. The--ago 7d --keep 10step is driven as aliteral, so the arm keeps firing the day the record is right.
🚨
--keepis removed rather than raised, and that is the point. It counts NEWER BUILDS, so arepository republished many times a day ages its own manifests out in hours whatever
--agosays —this issue's own root cause ("republishing frequency is what DESTROYS a pin") stated as a flag. A
30-day
--agobeside a--keep 10still collects a manifest ten builds old on the day it iswritten. The window is not offered as pin protection; the lock is. It is the decided policy, stated
where re-enabling would otherwise install a window nobody chose.
The record is deliberately AHEAD of the registry, and says so
Nothing was applied. The live task is disabled and still carries
--ago 7d --keep 10;tasks.jsongainsrecordAheadOfRegistry, and three things follow from the declaration rather thanfrom anyone remembering it:
verifyprints the declared drift instead of reporting it as an incident — a drift report that isalways red is one nobody reads, which this file learned on 2026-09-07 over a trailing newline —
and reds when a declared task shows NO drift, so the declaration expires by being checked.
record, the one command that overwrites the record from live, refuses without an explicitOVERWRITE: re-capturing would silently restore the quota window into a file whose commentsexplain at length why it must not say that.
Also closed: the record can no longer contradict itself
retention_windowsprints "PAUSED" only when it finds no enabled step, so a task flipped toEnabledwhilepause.inForcewas still true printed the window and never mentioned the pause —a record asserting both that cleanup is stopped and that it runs at 03:00, with nothing red. That is
now a failure, and so is every task
Disabledwith no declaration explaining it: a stoppedretention and a stale record otherwise read identically, and only one of them is a decision.
Still open here, unchanged
applyrefuses while the pausestands); the nightly deletion is still not downstream of that night's verdict. Three coordination
mechanisms are costed on Retention: interlock cleanup with protection updates and active publication #3859 with a recommendation; the choice is the maintainer's and needs
an RBAC grant on the shared registry.
storage: dropping
--keep 10and moving 7d → 30d retains strictly more over five repositoriesthat republish many times a day.
:latestquestion, theContainer Registry Repository Writergrant, released bundles andactive builds as consumers, the production read-back,
client-b's declaration, and the one-off statecleanup — all as recorded on 2026-09-12.
Measured 2026-09-13 (
date -u), read-only: no purge, no dry run, noapply, no registrymutation of any kind. Design:
Doc/Architecture/ArtifactRetentionInterlock.rbuergi commentedon Oct 11, 2026
(re-posted 2026-10-11; originally by @rbuergi on 2026-09-13)
🚨 The protection lane has been RED since 2026-09-13 01:18Z, and its stated reason is FALSE — #4221
Found while establishing "what would have to be true to re-enable safely". It is the answer:
pause.reEnableWhenis "lock-pinned-digests is green", and it is not.— run 34730206406
That overlay pins two images. The shipped extractors, run over the actual committed bytes:
deployments/aks/build/values.build.public.yamlcr.meshweaver.cloud(memex-portal-ai:3.0.0-ci.8411+ its migration twin)deployments/aks/memex/values.memex.public.yamlREGISTRY_HOST_REis(?P<host>[a-z0-9]+)\.azurecr\.io— right for a lane that locks one ACR, butit made "pins its images elsewhere" spell identically to "pins nothing". An instrument answering
confidently wrong, and the fix it suggests is to repair an extractor that has no defect.
The cost of the wrong label
Because the run refuses as a whole,
memex— six pins in this ACR, the installation actuallyexposed to the ACR purge — got no locks either. One instance's registry question held the
other's protection hostage, and the mislabelled red sat on the pause's own re-enable condition.
buildis the fleet's build server and the first live installation oncr.meshweaver.cloud.client-bpins there too and is declarednot-installed, which is why this surfaced only now.🚨 The larger finding, and it is an open item rather than a fix
A second registry now serves live installations, and
acr purgecannot reach it. Per thenew-deployment path,
cr.meshweaver.cloudis the default for new instances. Everything on thisissue — the lock, the closure, the tag lock, the 30-day window, the interlock — is about
meshweaver.azurecr.io. What retains the fleet's own registry is established by nothing, andthat gap grows with every instance provisioned there.
#4221 does not answer that. It makes the distinction legible: foreign references are extracted
and named, an undeclared one is RED naming the registry, a declared one is accepted and printed
on its own denominator line (
…declared OUT OF THIS REGISTRY'S SCOPE) so it can never be countedamong the protected.
registryininstances.jsonis a scope statement, never an exemption fromanswering — checked both ways against what the overlay actually pins, refused when it names a
registry the overlay does not use, refused when the installation also pins in this ACR (half in
is not out), refused on a non-live entry and refused without a reason.
Eleven arms, falsified in two directions. Sabotage A — the foreign extractor stubbed to answer
nothing, i.e. the pre-fix behaviour — reproduces this issue's own false sentence verbatim and reds
six of them.
Added to what is still the maintainer's
cr.meshweaver.cloud. Same question this issue answers for the ACR, askedagain about a second store. Nothing today says what keeps those images, or deletes them.
Design:
Doc/Architecture/ArtifactRetentionInterlock→ A second registry.Measured 2026-09-13 (
date -u), read-only: the run log over REST, the two overlays' committedbytes through this repository's own extractors, and the task record. No purge, no lock, no dry run,
no
apply, no registry mutation.rbuergi commentedon Oct 11, 2026
(re-posted 2026-10-11; originally by @rbuergi on 2026-09-13)
What landed, what is in flight, and the measured cost of the lane being red
Merged
7be4af5976) — the recorded purge now states No platform pins in module repos — build against the released platform, and be TOLERANT of drift (same major serves) #3842's decided window (at least 30 daysby age, no
--keepbuild-count quota) and a credential-free gate holds every recorded step toit on every pull request;
acr-retention-tasks.sh applyrefuses to re-enable a task while thepause stands. Verified live: the post-merge run of
lock-pinned-digests.ymlprinted"every recorded step retains for at least 30 days by age with no --keep build-count quota
(No platform pins in module repos — build against the released platform, and be TOLERANT of drift (same major serves) #3842), and the record's pause declaration agrees with the statuses it is recorded beside."
The cost of the lane being red, measured
Lock pinned image digests, last twelve runs:build(#4221)buildcauseSo no lock has been written in over two days, and
pause.reEnableWhen— "lock-pinned-digestsis green" — has been unmet since. Two consecutive nightly protection runs lost, for two different
causes. The purge being paused is what makes that survivable rather than an incident.
In flight
buildpins incr.meshweaver.cloud; the lane now names the registry instead of claiming the overlay "pins noimage at all", and
instances.jsongains aregistriesdisposition table (the unit is theregistry, not the installation, so the declaration can be shown to be complete).
now fails closed on a non-boolean or explicitly-null
inForce(one typed quote mark wouldhave disarmed the apply interlock, the overwrite guard and the coherence gate at once), and a
purge step is read as tokens rather than text —
--include-"locked"executes as that optionand matched no grep for it, which defeated the one invariant this lane rests on.
Still the maintainer's, unchanged
it needs an RBAC grant on the shared registry and the disabling of a destructive schedule.
cr.meshweaver.cloud. The fleet's own registry serves at least one liveinstallation and is the default for new ones;
acr purgecannot reach it, and nothing says whatkeeps — or deletes — what is in it. Same question this issue answers for the ACR, asked again
about a second store.
:latestquestion, theContainer Registry Repository Writergrant, released bundles andactive builds as consumers, the production read-back,
client-b's declaration, and the one-offstate cleanup — all as recorded on 2026-09-12.
Measured 2026-09-13 (
date -u), read-only throughout: no purge, no lock, no dry run, noapply, no registry mutation of any kind.rbuergi commentedon Oct 11, 2026
(re-posted 2026-10-11; originally by @rbuergi on 2026-09-13)
The second-registry question is now its own issue: #4230
My earlier comment on this thread listed "what retains
cr.meshweaver.cloud" as an open itemhere. It is now filed separately — #4230 — because it is not a part of this issue: everything
under #3438 (the lock, the closure, the tag lock, the 30-day window, #3859's interlock) is about
meshweaver.azurecr.io, and #4230 is the same question asked about the registry that is now thedefault for newly provisioned instances and that
acr purgecannot reach.What it carries, measured across all twelve of the Memex deployment overlays on 2026-09-13:
buildis LIVE and pins 0 images in this ACR and 2 incr.meshweaver.cloud;client-bthesame but not installed;
memex6/0;memex-cloud3/1 (the one beingghcr.io, third-party).This issue keeps the ACR-side items unchanged.
rbuergi commentedon Oct 11, 2026
(re-posted 2026-10-11; originally by @rbuergi on 2026-09-14)
Open item 3 — "what retains
cr.meshweaver.cloud" — is ANSWERED and moves off this listFrom the 2026-09-13 19:36Z comment, "Still the maintainer's":
Something says it now: #4230, landed as #4315, designed in
Doc/Architecture/FleetRegistryRetention.Nothing deletes there — six mechanisms enumerated, measured read-only over 409 files with the
sweep pattern proven on a synthetic control first — and nothing may, until a cleanup can derive a
protected set over what the registry holds. The remaining unknown is one Azure storage read, and it
is carried in the record as a state of its own rather than as a measured absence.
🚨 The part that bears on THIS issue: the rule over there is stricter, and the reason is structural
distributionhas no lock.changeableAttributes.deleteEnabled— the thing everything on thisissue rests on — is an ACR feature, so there is no object a protected set can be written into ahead
of a deleter.
That reframes what the lock buys here, and it is worth stating because it is easy to under-read: the
lock is not merely "the protection"; it is persistent state, written by a different process on a
different clock. That is why an incomplete run on this issue's lane is survivable — it writes fewer
locks and last night's still hold — and the lane could be red for two days in September without
anything already protected being at risk.
On a registry with no lock none of that holds, which is why #4230's rule makes "an incomplete
derivation deletes NOTHING" a clause rather than a nicety.
Also landed here, small
The nightly run no longer discards what it derives about the other registry. It prints a
PROTECTED SET — registries this lane cannot locksection — repository, tag and the file that pinseach (4 references,
build×2 andclient-b×2) — with its own incompleteness on its own line. Itlocks nothing and needs no new credential; on a lock-less registry the derivation is the only
artifact available, and a derivation nobody can read is not one.
The
registriestable'scr.meshweaver.cloudentry no longer says "established by nothing"; theout-of-scope report line still says UNVERIFIED, and must — this lane still protects nothing
there.
Unchanged on this issue
Items 1, 2 and 4 of that comment stand exactly as recorded: #3859's coordination mechanism (an
RBAC grant on the shared registry and the disabling of a destructive schedule — the maintainer's),
applying the decided window to the disabled task (the same act as lifting the pause), and the
:latestquestion, theContainer Registry Repository Writergrant, released bundles and activebuilds as consumers, the production read-back,
client-b's declaration and the one-off state cleanup.Measured 2026-09-14 (
date -u), read-only throughout: no purge, no lock, no dry run, noapply, no registry mutation of any kind, on either registry.🤖 Generated with Claude Code
rbuergi commentedon Oct 11, 2026
(re-posted 2026-10-11; originally by @rbuergi on 2026-09-14)
Landed —
e73047d3233cc4816fd15735528a4766feb8d8d5(#4315). Open item 3 is off this listThe "NEW: what retains
cr.meshweaver.cloud" item from the 2026-09-13 19:36Z comment is answeredand moved to #4230, where the close-out lives. Nothing on this issue's ACR side changed.
🚨 One correction that belongs here too, because this thread carried the same framing: the
danger at the fleet registry is unbounded growth, not silent deletion. Nothing deletes there —
six mechanisms enumerated, read-only, with the sweep pattern proven on a synthetic control first.
That reverses what "retention is established by nothing" implied.
The part that bears on THIS issue, and it is worth reading twice
distributionhas no lock.changeableAttributes.deleteEnabled— the thing everything on thisissue rests on — is an ACR feature.
That reframes what the lock actually buys here. It is not merely "the protection": it is
persistent registry state, written by a different process on a different clock. That is why an
incomplete run on this lane is survivable — it writes fewer locks and last night's still hold — and
why this lane could be red for two days in September without anything already protected being at
risk. None of that holds on a store with no lock, which is why #4230's rule makes "an incomplete
derivation deletes NOTHING" a clause rather than a nicety.
Also landed here, small
The nightly run no longer discards what it derives about the other registry: it prints a
PROTECTED SET — registries this lane cannot locksection (repository, tag, and the file that pinseach — 4 references,
build×2 andclient-b×2) with its own incompleteness on its own line. It locksnothing and needs no new credential; on a lock-less registry the derivation is the only artifact
available, and a derivation nobody can read is not one.
The
registriestable'scr.meshweaver.cloudentry no longer reads "established by nothing" — butthe out-of-scope report line still says UNVERIFIED, and must: this lane still protects nothing
there.
Unchanged and still open on this issue
destructive schedule. Costed on that thread with a recommendation; the choice is the maintainer's.
:latestquestion, theContainer Registry Repository Writergrant, released bundles andactive builds as consumers, the production read-back,
client-b's declaration and the one-off statecleanup — all as recorded on 2026-09-12.
Measured 2026-09-14 (
date -u), read-only throughout: no purge, no lock, no dry run, noapply, no registry mutation of any kind, on either registry.🤖 Generated with Claude Code
rbuergi commentedon Oct 11, 2026
(re-posted 2026-10-11; originally by @rbuergi on 2026-09-15)
Triage 2026-09-15 — (c) tracking, NOT complete. Stays open, and two things in the body are now misleading enough to correct.
The two comments of 2026-09-14 already close out open item 3 and end "Unchanged and still open on this issue"; this adds only what they do not say.
1. 🚨 All four checkboxes in the "Current delivery scope" block are stale — three are done, and the list is no longer the denominator
cr.meshweaver.cloud(spun out 09-14)Anyone reading the body today sees three unchecked boxes that are done and misses one open child that is not there. Worth an edit.
2. 🚨 #3859 reads closed-completed while this parent still lists its coordination mechanism as open
The 09-14T14:02Z comment enumerates as still open: "#3859's coordination mechanism — the RBAC grant on the shared registry and the disabling of a destructive schedule … the
:latestquestion, theContainer Registry Repository Writergrant, released bundles and active builds as consumers, the production read-back,client-b's declaration and the one-off state cleanup." A closed child whose mechanism the parent lists as outstanding is the kind of pair that makes a later sweep close the parent on "all children green". Either #3859 reopens or the parent stops citing it.3. The three reasons the parent is genuinely not closable — all measurable in-repo
.github/acr-retention/purge-old-images.yaml:47-52says so against itself: "THE RECORD IS AHEAD OF THE REGISTRY HERE, ON PURPOSE… The live task is DISABLED (paused, Memex#219) and still carries--ago 7d --keep 10;acr-retention-tasks.sh applyis what pushes this file onto it." The record is now--ago 30d --untaggedwith--keep 10removed, not raised (:64) — correct, and not yet what the registry runs. The parent's own unchecked verification item is exactly this.lock-pinned-digests.yml(cron: 0 1 * * *) →lock-pinned-digests.py, whose premise is "acr purgeSKIPS LOCKED MANIFESTS BY DEFAULT" (:21); the 09-12 and 09-13 schedules both failed, andpause.reEnableWhen= "lock-pinned-digests is green" has been unmet since.For the record: the pin-protection half asked for in the filing does exist in core now, on all three axes
.github/workflows/lock-pinned-digests.yml+.github/scripts/lock-pinned-digests.py— committed workflow digest pins, deployment-overlay tag pins, and (since #3438: retention protects what is CONSUMED, and refuses when it cannot see it all #4081) each installation's own/api/version. Locks manifest and tag separately (a tag readdeleteEnabled: trueover a manifest readingfalse)..github/workflows/pinned-digests.yml+check-pinned-digests.py, with credential-free arms on every PR viadotnet-test.yml:2205/2221/2250/2259/2277..github/acr-retention/), so the "two tasks diverged and nobody noticed" defect the filing calls out is structurally addressed — the two live tasks agreeing with the two records is the remaining verification.So the filing's preferred option landed and its rejected option (raising
--ago/--keep) was not taken. What is left is delivery, not design.Read-only: REST issue states, local reads of
.github/on coremain. No writes, nothing applied.rbuergi commentedon Oct 11, 2026
(re-posted 2026-10-11; originally by @rbuergi on 2026-09-15)
Two corrections to the 2026-09-15T05:09Z comment, and the cause of the current red: a new fleet repo, filed nowhere
Read-only re-measurement, 2026-09-15T08:0xZ, REST only.
Correction 1 — "the protection lane has written no lock since 2026-09-11" is wrong. Run history for
lock-pinned-digests.yml:The 09-14 schedule run is a complete green apply:
mode: apply (event: schedule)→Protected 16 manifest(s) (0 newly locked this run) and 13 tag reference(s) (0 newly locked); 0 released.That matters beyond bookkeeping:pause.reEnableWhenis "lock-pinned-digests is green…", so the re-enable condition was met on 2026-09-14, and the decision that turns on it is now live rather than pending.Correction 2 — even the failing runs write locks.
lock-pinned-digests.py:1778-1780is explicit ("Locks are applied even on a degraded run"), and run 34916617427 logs 15 writes at 01:22:52Z — 7locked <repo>@sha256:…plus 8locked tag …, includingmemex-portal-ai:3.0.0-ci.8612, the set memex is currently running. So the protection has not lapsed; the run's verdict has.The cause of the red, which is on no thread in this repo. Both failure annotations on run 34935956561, verbatim:
Systemorph/ClientA.Memexwas created 2026-09-14T21:30:38Z — about 4 h after the last green run, and ~20 h after the green schedule run above. A new deployments repo entered the fleet and reddened the fleet's only registry-protection lane. This is a real (b) inside this tracking issue:.github/scripts/lock-pinned-digests.py:1075-1079—build_instanceskeys installations by the bareHosting__Deploymentid across the whole org, so two repos each declaringmemexcollide;:1096-1101— a live instance with no ingress host getsinstance.error→ AXIS 3 INCOMPLETE → exit 1;.github/acr-retention/instances.json—instances[]holds onlybuildandclient-b; there is noclient-aentry.Fix sketch. Declare
client-aininstances.jsonwith its honeststate(not-installed/retired, orliveonce it has an ingress host) and a reason, exactly the wayclient-bis declared — that alone clears the second error and is the stricter-by-default direction the file already documents. The first error is a modelling gap and needs code: qualify the instance identity by its source repo (f"{scan.gh_repo}:{ident}", or arepofield on the roster entry) so two deployments repos can each declare amemexwithout one holding the other's protection hostage; keep the duplicate-id blocker for a genuine collision within one repo. Pair it with a roster arm that reds when a repo's overlay set is unrepresented, so the qualification cannot silently exempt anything — otherwise the fix for "a fork reddens the lane" becomes "a fork is invisible to it".What is still open here, unchanged. The decided 30-day window is record-only:
.github/acr-retention/purge-old-images.yaml:47-52says so against itself — the live task is DISABLED and still carries--ago 7d --keep 10, while the record is--ago 30d --untaggedwith--keepremoved (:64-69) andtasks.jsondeclaresrecordAheadOfRegistry.inForce: true. Applying it is the act that lifts the pause, and the instrument isaz acr task show --registry meshweaver --name purge-old-imagesthenacr-retention-tasks.sh apply. Also still open: the production read-back of the report receiver (#3858's last criterion —@Hosting/Modules/*reads[]for me, RLS caveat), and released module bundles / active builds as registry-level consumers (FleetRegistryRetention§5.3 —DeploymentPinnedReferences.Resolveyields platform builds only, no registry repository and no bundle digest). Body hygiene: #3858, #3860, #3859, #3849, #3843 and #3846 are all closed while three boxes are still unchecked, and #4230 is open and not listed.rbuergi commentedon Oct 11, 2026
(re-posted 2026-10-11; originally by @rbuergi on 2026-09-15)
2026-09-15 17:4xZ:
193e54ed4fcleared half of thelock-pinned-digestsred. The other half is a fact nobody has stated yet.The newest run, 34956358066 (10:08Z, after
193e54ed4f), fails on exactly one line:The roster now declares
Systemorph/ClientA.Memex:client-a, the TODO template, asnot-installed, and that one correctly stays quiet. But ClientA.Memex carries a second overlay,deployments/aks/memex/values.memex.yaml, and that one is not a template:memexaksacrqoqqdqnhlaksg.azurecr.io/memex-portal-ai:3.0.0-ci.8622, from ClientA's own ACR, so it is a foreign registry and never lockable here;client-a.meshweaver.cloud, which resolves (57.174.148.54) and does not answer. From here, at 17:4xZ,curl --max-time 15timed out with http 000.No roster state is honest without knowing whether that portal is up:
not-installedwould be a false claim if it is running behind an allowlist in ClientA's estate;livekeeps the blocker, which is correct only if it is supposed to answer from a GitHub runner.Nothing is changed here, because this is a statement about a client estate that only its owner can make.
Until it is answered, the lane stays red on every push to its paths and on the 01:00Z schedule. Because
pause.reEnableWhenis "lock-pinned-digests is green", it is holding the re-enable decision again. The locks themselves are still written on a degraded run (lock-pinned-digests.py~1778).🤖 Generated with Claude Code
rbuergi commentedon Oct 11, 2026
(re-posted 2026-10-11; originally by @rbuergi on 2026-09-17)
2026-09-17 — the protection is holding nightly; both lanes are red for a third night on something that is NOT retention
The "one production action" from the 2026-09-07 comment is long done. It was the
Container Registry Repository Writergrant (the data actionMicrosoft.ContainerRegistry/registries/repositories/metadata/write, held by none of the three SPsat the time). It landed 2026-09-08 — first successful apply, run
34176008757, "Protected 18
manifest(s) (8 newly locked this run); 0 released".
And the lane is still writing. Run
35169861024 (2026-09-17T01:15Z,
mode: apply):That also discharges the 09-12 item "20 tag references are unlocked right now": 26/26 and 34/34 read
protected today. (
locked and pinned by NOTHINGhas grown 16 → 57 since 09-08 — the standing debt ofa lock-only job, unchanged in kind.)
🚨 Both lanes have failed three nights running, on one cause, and it is not retention
lock-pinned-digests.yml(cron: 0 1 * * *): 09-1534916617427, 09-1635043350450, 09-1735169861024. Sole annotation each night: "AXIS 3 — installationSystemorph/ClientA.Memex:memexcould not be accounted for:https://client-a.meshweaver.cloud/api/version could not be read" (
timed out09-16,Name or service not known09-17).pinned-digests.yml(cron: 20 5 * * *): 09-1534933010021, 09-1635059768252, 09-1735185997013. Annotations name only that repo's twoPLATFORM_REFdeclarations — while its owncounters in the same run read declarations GONE 0, UNPROTECTED 0, INDETERMINATE 0.
Measured here 2026-09-17, and it contradicts both runners:
client-a.meshweaver.cloudresolvesto 74.158.7.121 (it was 57.174.148.54 on 09-15 — the record moved) and
GET /api/versionanswers200 with
{"version":"3.0.0+2592e2284c8b34d91322e2d4e6998d17ca113821"}. The portal is live andanswering; what has not answered for three nights is the GitHub runner's reach to it. Its images
live in
memexaksacrqoqqdqnhlaksg.azurecr.io, declaredout-of-estatein theregistriestable, sonothing this lane would lock is at stake either way — yet
pause.reEnableWhenis "lock-pinned-digestsis green", so this is holding the re-enable decision hostage for a third night. Nothing is filed
for it.
Still owed, unchanged
.github/acr-retention/tasks.json→recordAheadOfRegistry.inForce: true,since: 2026-09-13;purge-old-images.yaml:64-69is therecord (
--ago 30d --untagged,--keepremoved) while the live task is Disabled and stillcarries
--ago 7d --keep 10. Applying it is the same act as lifting the pause — system-changing.criterion).
FleetRegistryRetention§5.3:DeploymentPinnedReferences.Resolveyields platform builds only,no registry repository and no bundle digest. Not built.
:latestquestion — a moving tag is named by no file and sits outside the lock model.Body hygiene: all four "Current delivery scope" boxes are unchecked while #3858, #3860, #3859,
#3849, #3843 and #3846 are all closed/completed, and #4230 is open and not listed.
Verification of the earlier halves on today's
main:9dfce19c0= #3462 →pinned-digests.yml+check-pinned-digests.py;deccceceef607ac9038c08a3a80cfc7d155b9482= #3507 →lock-pinned-digests.yml+.py. Both present.--check-retention-recordrun locally: EXIT=0.rbuergi commentedon Oct 11, 2026
(re-posted 2026-10-11; originally by @rbuergi on 2026-09-18)
Triage 2026-09-18 — still open, and two inputs to this issue have moved since the 09-17 comment
Read-only: core
origin/mainb76f57a79d, GitHub REST, and three unauthenticated/api/versionGETs. No registry call, no purge, no lock, no cluster access.The remaining item is unchanged — measured in-repo, not inferred
.github/acr-retention/tasks.jsononorigin/maintoday:purge-old-images→"status": "Disabled", note "DISABLED 2026-09-12 — see the top-levelpauseblock"recordAheadOfRegistry.inForce: true,since: "2026-09-13"→ "purge-old-images.yaml step 1 records--ago 30dwith no--keep; the live (disabled) task still carries--ago 7d --keep 10"pause.inForce: true,reEnableWhen: "lock-pinned-digests is green …"So the decided 30-day window is in the record and not in the registry, which is exactly what this issue's title asks for. Nothing to adjudicate into a close.
🚨 1. The lock lane is red a FOURTH consecutive night, and the cause has CHANGED
The 09-15 and 09-17 comments name the cause as "
client-a.meshweaver.cloudcould not be read: URLError: timed out". That is no longer it. Run 35294596409's annotations, verbatim:Two things follow. The host answers now (
curl --max-time 15 https://client-a.meshweaver.cloud/api/version→http=200, 2026-09-18 06:14Z), so the timeout half is resolved and the lane read a real core sha from it. And the registry it pins is not thememexaksacrqoqqdqnhlaksg…named in the 09-15 comment — a second, undeclared ClientA ACR. The remedy is aregistriesdeclaration, not a roster state change. This is filed on no issue in either repo (search/issuesforClientAand forlock-pinned-digests,state:open→ only #3355, the release tracker).Because
pause.reEnableWhenis "lock-pinned-digests is green", this is what currently stands between the fleet and re-enabling the purge — i.e. between this issue and its last item.🚨 2. The roster's
client-bentry is now factually wrong, and it exempts a LIVE installation.github/acr-retention/instances.json:306-310still reads:Measured 2026-09-18 06:14Z:
and
Systemorph/Memex@mainmesh/Deployments/client-b.jsonreadsstatus: "Live"(commitf86d9cc78, 2026-09-16 — "client-b: status Live — provisioned end to end through the Hosting plugin") withupdatePolicy: "Continuous"(bdf675ddf, 2026-09-17). The entry's own instruction — "Delete this entry the day it is provisioned" — came due two days ago. Until it is, AXIS 3 exempts an installation that is serving.(For the record,
67cbbe0eeis an ancestor ofmainand 1871 commits behind it, on an instance declaredContinuous. That is its own question and not this issue's.)What this issue still owes, unchanged
The decided 30-day window applied to the paused
purge-old-imagesand read back from production, plus the bundle/active-build consumer inventory. Both are gated on the lane being green, which items 1 and 2 above now describe.Read-only triage 2026-09-18 by Claude Opus 5 (1M context). No registry, cluster or deployment mutation of any kind.
rbuergi commentedon Oct 11, 2026
Step 4 is done: production has been read back through the governed path. The
RegistryRetentionaction kind is live on the control instance, and its dry run answered today (2026-10-11, requestOps/Actions/registry-retention-verify-20261011-3438, operations-lane run 38141799080, read-only, no approval).What the verify printed:
purge-old-imagesrecordAheadOfRegistry). Live step 1 is still--ago 7d --keep 10 --untagged; the record's is--ago 30d --untagged. Step 2 (memex-portal,--ago 30d) agrees.purge-old-ci-releasescompared 2 task definition(s) against the record3 purge step(s) inspected; none passes --include-locked, so locked manifests are skippedpreflight: nothing in the record's interlocks refuses an applySo the 30-day window is still not installed on the registry, and nothing has changed there since the 2026-09-22 reading. The same run answered the storage lifecycle row for the fleet registry (
present=false); that is recorded on #6499 and in #6502.What remains is the maintainer's, in this order. No agent approves a governed action.
Container Registry Tasks Contributoron the registry (steps 2 and 3 of the 2026-10-10 comment: the ABAC re-grant, thenInfraDeploywhat-if and deploy). The verify needed onlyReader, so today's green run does not show whether that grant has landed. I did not read role assignments.RegistryRetentionfor deploymentcontrolwithdryRun: falseandconfirmation: control, and approve it. This installs--ago 30dwith no--keeponpurge-old-images, which stays disabled.recordAheadOfRegistryfrom.github/acr-retention/tasks.json(it goes stale by design once the apply lands).lock-pinned-digests.ymlonmainis not green and recent today: the newest run (38101969700, schedule, 01:28Z) was cancelled, the one before it (38091102891) failed, and the last success is 38087852774 at 2026-10-10 21:30Z. The apply refuses an enable until the newest run is green and under 36 hours old.Leaving this open until step 2 is read back with another verify.