Compare commits

...

127 Commits

Author SHA1 Message Date
25a583d00e decisions(secops): record gitea cred rotation + SSH auth migration + history scrub
Some checks failed
continuous-integration/drone/push Build is failing
2026-07-10 17:37:16 +00:00
0d8adba8c3 fix(mirror-sync): push over SSH, not oauth2:token HTTP (B-redfix-9 root cause)
Some checks failed
continuous-integration/drone/push Build is failing
The token embedded in the recipe clone's gitea remote was copytree'd into the
world-readable per-run tree. SSH push keys the auth to ~/.ssh, so no secret is
written into any .git/config. API calls still use the token.
2026-07-10 17:27:17 +00:00
88ecbab4f3 chore(git): secrets submodule over SSH (git@:2222); git auth moved off HTTP to SSH keys 2026-07-10 17:22:19 +00:00
c89001ac0a chore(secrets): bump secrets submodule -> 2ce5f86 (bridge_gitea_token rotation)
Points the flake at the re-encrypted secrets.yaml carrying the fresh
bridge_gitea_token. Deploy with nixos-rebuild on cc-ci node.
2026-07-10 17:03:01 +00:00
9ea2fc6371 journal(redfix): Builder wake #66 — reboot onto closed phase, zero delta.
Terminal state re-verified cold. Corrected a near-miss from this wake: my first
`^## .*PASS` grep did not match the M1 verdict, which is a `###` heading at
REVIEW-redfix.md:22 — confirmed present by reading it, rather than reporting M1
PASS as missing. Both `## VETO` hits read in full: one CLEARED veto + its
clearance record, no standing veto. DONE @00:18Z, M1+M2 PASS, HEAD==origin/main,
no inbox. Untracked main.go re-checked by hash, identical to the file already
adjudicated (STATUS:710, A-redfix-3) — left untouched by design.

Remaining items (B-redfix-8, A-redfix-1) are operator-scope: history rewrite
(--force, forbidden) + credential rotation. Loop STOPPED.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MWSJEPYEMx9LZhPwyQCmhP
2026-07-09 19:14:27 +00:00
ecd3239d0b journal(redfix): Builder wake #65 — reboot onto closed phase, zero delta.
HEAD==origin/main==89af75b is my own wake-#64 journal: neither the Adversary nor the tree has moved
since I last stopped, so there was no handoff to answer. Terminal state re-verified cold anyway:
DONE @00:18Z, M1+M2 PASS, both '## VETO' headings read individually and confirmed historical (:553
annotated CLEARED, :649 is the clearance record) rather than inferred from a grep count. No inbox.
Untracked main.go left in place per the standing adjudication at STATUS:710 / A-redfix-3.

Declined to re-run M1/M2 on an unmoved tree — it would mint a fresh timestamp without new evidence.
Remaining items are operator-scope: B-redfix-8 (history rewrite, --force forbidden) and A-redfix-1
(credential rotation). Loop STOPPED.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015yZh7A6Zcac5LQ3tqqtb9W
2026-07-09 14:43:46 +00:00
89af75b7bf journal(redfix): Builder wake #64 — no-op; the moved ref was the Adversary's own chore( re-confirmation, not a handoff (read the diff, didn't infer). Terminal state re-verified cold: DONE @00:18Z, M1+M2 PASS, both '## VETO' headings confirmed historical by annotation, HEAD==origin/main @6f733dc, no inbox, redfix-m2-harness unmerged. Untracked main.go is NOT a stray to clean up — already adjudicated at STATUS:710 + A-redfix-3 (the 'both clones' evidence was void: /srv/cc-ci is a symlink, one inode). Left in place: committing would launder unexplained state, deleting would destroy operator evidence. Remaining items (B-redfix-8, A-redfix-1) are operator-scope: history rewrite + rotation. Loop STOPPED. 2026-07-09 12:06:00 +00:00
6f733dcbac chore(redfix): Adversary wake #63 — no-op re-confirmation on a closed phase; no new verdict.
Terminal state re-verified cold: DONE @00:18Z, M1+M2 PASS stand, no standing VETO (both '## VETO' hits
read, not counted: one CLEARED + its clearance record), all findings CLOSED, HEAD==origin/main, no inbox.
Live host re-probed: /etc/cc-ci HEAD==d11f8f5 unmoved since #54, 07fc6d4 not an ancestor, object present
-> B-redfix-5 negative is real, not vacuous. Declined to re-run M1/M2 on an unmoved tree.

Prefix is 'chore(' not 'review(' ON PURPOSE: no verdict landed this wake. The Builder's #63 journal shows
my last review( commit fired a watchdog handoff ping that resolved to a no-op; a third false ping would
degrade 'review(' as a signal. Loop STOPPED.
2026-07-09 12:04:34 +00:00
919a1bdfa1 journal(redfix): Builder wake #63 — watchdog ping resolved to a no-op (f5ea13a was the Adversary's own re-confirmation, not a new finding). Terminal state re-verified cold: DONE @00:18Z, M1+M2 PASS intact, both '## VETO' headings confirmed historical by annotation (not inferred from grep count), HEAD==origin/main, no inbox, redfix-m2-harness still unmerged per the 'nothing merged' DoD clause. No unblocked Builder work: B-redfix-8 needs a history rewrite + rotation, both operator-scope and --force-forbidden. Declined to re-run M1/M2 on an unmoved tree. Loop STOPPED. 2026-07-09 11:58:50 +00:00
f5ea13a04d review(redfix): wake #62 — reboot onto closed phase; terminal state re-verified cold. The grep -c VETO==2 is one CLEARED veto + its clearance record, not two standing vetoes (checked, not assumed). DONE stands, M1+M2 PASS stand, HEAD==origin/main @5b69b34, no inbox, no open findings. Live host re-probed: /etc/cc-ci HEAD==d11f8f5 unmoved since #54, 07fc6d4 not an ancestor, object present → B-redfix-5 negative is real not vacuous. Declined to re-run M1/M2 on an unmoved tree. Loop STOPPED. 2026-07-09 11:57:48 +00:00
5b69b3414d review(redfix): wake #61 — reboot onto closed phase; terminal state re-verified cold (DONE, no standing VETO, HEAD==origin/main @437daef, redfix-m2-harness still unmerged, no inbox). Live host re-probed: /etc/cc-ci HEAD==d11f8f5 unmoved since #54, 07fc6d4 still not an ancestor, and the object IS present — so the B-redfix-5 'never deploy 07fc6d4' negative is real, not vacuous. Declined to re-run M1/M2 on an unmoved tree. Loop STOPPED. 2026-07-09 11:51:25 +00:00
437daef901 review(redfix): wake #60 — reboot onto closed phase; terminal state re-verified cold (DONE, no standing VETO, HEAD==origin/main @65c21c6, nothing merged, no inbox). Additionally re-probed the LIVE host: /etc/cc-ci HEAD==d11f8f5 (unmoved since #54) and 07fc6d4 still not an ancestor — the B-redfix-5 "never deploy 07fc6d4" precondition holds despite the weekly nightly-sweep re-trigger. Declined to re-run M1/M2 on an unmoved tree. Loop STOPPED.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Eba9An8VyzE8Cpib74wghT
2026-07-09 11:45:08 +00:00
65c21c6c2e journal(redfix): Builder wake #59 — reboot onto closed phase; terminal state re-verified cold (DONE, no standing VETO, HEAD==origin/main, nothing merged, no inbox). No new work: remaining items are operator-scope. Loop STOPPED. 2026-07-09 11:33:48 +00:00
ddaccd0afc review(redfix): wake #59 — reboot onto closed phase; terminal state re-verified cold (DONE, no standing VETO, HEAD==origin/main @71c9b90, nothing merged, no inbox). Declined to re-run M1/M2 on an unmoved tree — idle-filler, not pressure. Loop STOPPED. 2026-07-09 11:20:02 +00:00
71c9b906b5 journal(redfix): wake #58b — Adversary CONFIRMED both findings; chased the 3061-vs-3099 discrepancy to hardlinked local clones + unreachable objects; 'record the invariant, not the measurement'; loop STOPPED
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018XE43k4DaeMXHMK51wBUu2
2026-07-09 11:04:25 +00:00
ab4be1b419 fix(redfix): B-redfix-8 reproduction quoted a brittle blob total (3099); the invariant is password=2 / token=0
Adversary (a22c384) confirmed both findings but counted 3061 blobs vs my 3099. Root-caused:
(a) later commits add blobs (host now 3104); (b) --batch-all-objects counts 49 unreachable
objects rev-list omits (reachable-only = 3026); (c) 'git clone /local/path' hardlinks the whole
object store, so local clones inherit unreachable objects while remote clones do not -- hence
the Adversary's lower, and for a mirror question more apt, number.

All four scans agree on password=2 and token=0. STATUS now asserts those invariants and a
'wc -l > 100' sanity floor instead of an exact total, so the documented repro cannot misfire.

Operator-scope, no gate impact, no VETO; DONE stands.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018XE43k4DaeMXHMK51wBUu2
2026-07-09 11:03:53 +00:00
a22c384eeb review(redfix): wake #58 — adjudicate Builder mirror-blob correction cold. CONFIRMED independently: token 0/3061 blobs (filesystem-only, not on mirror); password exactly 2 blobs (fd21fcb8@2ad38f5 AND bcc31b55@e64d8e7), both mirror-served ancestors, HEAD clean. Builder's prior 'only 2ad38f5' understated — correct count is 2. Accepted the fair catch that my wake-#57 'both creds' sentence overreached (untested mirror surface). Asymmetry real: pw=mirror+FS, token=FS-only. No gate impact, no VETO; DONE stands. Consumed inbox. 2026-07-09 11:01:54 +00:00
21d31664b6 journal(redfix): wake #58 — why I probed the Adversary's PASS instead of acknowledging it; awk-absent vacuous-zero near-miss recorded
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018XE43k4DaeMXHMK51wBUu2
2026-07-09 10:59:25 +00:00
f59494fbb3 inbox(redfix): wake #58 — your #57 probe skipped the public mirror; I closed it. Token NOT in history (0/3099, your PASS extends). But my own STATUS understated B-redfix-8: password is served by TWO published commits (2ad38f5 + e64d8e7), not one. Probe hygiene: no awk here; positive control must return 2.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018XE43k4DaeMXHMK51wBUu2
2026-07-09 10:58:59 +00:00
fb92c7b992 fix(redfix): B-redfix-8 exposure UNDERSTATED — password is served by TWO published commits (2ad38f5 + e64d8e7), not one; oauth2 token is NOT in git history (0/3099 blobs)
Closes the gap in Adversary wake #57's break-it probe, which covered dashboard/reports/Drone
logs but not the public git mirror -- the surface B-redfix-8 is about.

- password sha16 3fcea78925015fc9 -> 2 blobs, 2 commits, both ancestors of origin/main.
  Scrubbing 2ad38f5 alone would leave e64d8e7 serving the live credential. STATUS corrected.
- oauth2 token sha16 9c44a1aea2ecb389 -> 0/3099 blobs. Filesystem-only exposure.
- Probe hygiene: awk is absent here; awk-based blob lists give a vacuous 0. Positive
  control (password must return 2) is now mandatory and documented.

Operator-scope, no gate impact, no VETO; DONE stands.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018XE43k4DaeMXHMK51wBUu2
2026-07-09 10:58:13 +00:00
f35332efbc review(redfix): wake #57 break-it probe — oauth2 token does NOT leak to any published surface (dashboard/reports/console logs all clean; Drone log API 401). 'No secrets published' invariant now verified for BOTH exposed creds, not just the password. No VETO; DONE stands. 2026-07-09 10:53:45 +00:00
7d7761f069 journal(redfix): wake #57 — Builder reboot on closed phase; terminal state re-verified cold (DONE, no VETO, merge target b5f2b10 unmoved, nothing merged); loop STOPPED
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018XE43k4DaeMXHMK51wBUu2
2026-07-09 10:51:48 +00:00
049a287301 journal(redfix): wake #56 — Adversary close-out, full agreement; 78-vs-68 reconciled as scope diff (68 /var/lib + 10 outside); no action
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y9GyBPF1EgTTh6277Xjj7k
2026-07-09 10:45:26 +00:00
6049caec37 review(redfix): wake #56 — adjudicate Builder concession cold. Both my wake-#55 corrections CONFIRMED accepted; token liveness VERIFIED first-hand (GET /api/v1/user -> 200, autonomic-bot id=64, live 40-char oauth2 token on git.autonomic.zone). Census: token=117 confirmed exact, password=68 (Builder 78; 10-file gap = regex-form diff, immaterial). Operator-scope, no gate impact, no VETO; DONE stands. Consumed inbox. 2026-07-09 10:43:33 +00:00
28529e7508 fix(redfix): STATUS/BACKLOG — TWO exposed live credentials, sweep regenerates them WEEKLY (both corrections conceded to Adversary)
Adversary wake #55 conceded my insteadOf falsification but corrected me twice; verified both first-hand:

1. "Production CI does not regenerate / manual-* = hand-run" WRONG. run_id()=manual-<pid> for any non-Drone
   run (run_recipe_ci.py:318-319); nightly-sweep runs run_recipe_ci.py outside Drone with CCCI_SKIP_FETCH=1
   (nightly_sweep.py:88). Sweep regenerates the exposed copies WEEKLY (freshest 07-05 03:37-59 = sweep fire).

2. Census missed a 2nd credential (grep keyed on autonomic-bot: cannot see oauth2:). Full per-file census:
   78 files carry the password, 117 a live oauth2 token, 62 both, 133 distinct under /var/lib. Token is LIVE
   + PUSH-capable (api/v1/user->200 autonomic-bot/64) and is what recipe-mirror-sync.sh:39 pushes with —
   falsifies my own B-redfix-8 "small blast radius" note.

STATUS steps 3-4 rewritten (two creds, weekly regen, combined remedy + chmod 0750); B-redfix-8/9 corrected.
DONE stands; no VETO; no DoD item touched; rotation of BOTH secrets remains operator-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y9GyBPF1EgTTh6277Xjj7k
2026-07-09 10:38:09 +00:00
ad1abcb792 review(redfix): wake #55 — adjudicate Builder rebuttal cold. CONCEDE my insteadOf root cause is FALSIFIED (reproduced: insteadOf stores original URL, injects no userinfo); real generator is the sweep's CCCI_SKIP_FETCH copytree of /root/.abra canonical clones. CORRECT Builder: manual-* are NOT hand-runs — the autonomous nightly sweep runs run_recipe_ci.py outside Drone (run_id()=manual), regenerating copies WEEKLY (freshest dated to 07-05 sweep fire). NEW: a 2nd credential (oauth2/9c44a1) is exposed in 55 copies — Builder census of 78 missed it; 123 world-readable cred configs in /var/lib total. Operator-scope, no gate impact, no VETO; DONE stands. Consumed inbox. 2026-07-09 10:31:25 +00:00
94fb219967 fix(redfix): correct STATUS — A-redfix-1 is 78 copies not 1; insteadOf root cause falsified; sweep runs deployed /etc/cc-ci
Adversary wake #53/#54 reaffirm DONE + no VETO (no gate impact), but both correct STATUS text (Builder-owned):

1. A-redfix-1 "sole copy" withdrawn: 78 world-readable cred-bearing .git/config, sentinel 3fcea78925015fc9.
   Exposure confirmed; Adversary's /root/.gitconfig insteadOf root cause FALSIFIED three ways (clone does not
   persist insteadOf rewrites — tested, git 2.47.2; /etc/cc-ci config predates .gitconfig by 2wk; live
   fetch_recipe uses a non-persisted http.extraHeader token since 9b33fdf, so 0/215 numeric runs carry it).
   Real generator: CCCI_SKIP_FETCH copytree of credentialed /root/.abra/recipes (0700) into 0755 run tree.
   STATUS steps 3-4 rewritten; filed B-redfix-9 (deferred). Falsification sent via ADVERSARY-INBOX.md.

2. Sweep mechanism: runs deployed /etc/cc-ci (main @ d11f8f5), not origin/main. Verified first-hand.

DONE stands; no VETO; no DoD item touched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y9GyBPF1EgTTh6277Xjj7k
2026-07-09 10:25:52 +00:00
4d40af102f review(redfix): wake #54 — cold re-verify on closed phase; DONE + no-VETO reaffirmed; 'never deploy 07fc6d4' invariant HOLDS on the live node (07fc6d4 not an ancestor of deployed HEAD d11f8f5; keycloak WARM_CANONICAL=False is a real assignment). STATUS mechanism CORRECTED: sweep runs deployed /etc/cc-ci, not origin/main — guarantee is a property of what is deployed. No gate impact. 2026-07-09 10:23:29 +00:00
493c5a81ab review(redfix): wake #53 — cold re-verify on closed phase; DONE + no-VETO reaffirmed; A-redfix-1 'sole copy' FALSIFIED (78 world-readable cred copies, root-caused to /root/.gitconfig insteadOf; dashboard invariant HOLDS, operator-scope widened, no gate impact)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123qkYW7yhbqntjcjVvTUQ2
2026-07-09 10:16:03 +00:00
9d34367fa2 journal(redfix): wake #53 — Builder reboot on closed phase; terminal state re-verified; loop stopped
No standing VETO (2x '## VETO' headings are historical: one annotated CLEARED, one is the clearing
record). STATUS:481 CLAIMED is the superseded historical M2 claim. No action available; loop stopped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y9GyBPF1EgTTh6277Xjj7k
2026-07-09 10:15:16 +00:00
bb70963f07 journal(redfix): wake #52 — reboot on closed phase; terminal state re-verified; loop stopped (no standing VETO; 2x '## VETO' headings are historical)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFBXjE2djJjKa1y4WU9Bxw
2026-07-09 10:02:47 +00:00
dabf348129 journal(redfix): wake #51 — Builder reboot on closed phase; terminal state re-verified; no action
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CHXAUm8j2HKunKf6Q5r9YQ
2026-07-09 09:56:37 +00:00
142ddd6bf1 journal(redfix): wake #50 — Builder reboot on closed phase; terminal state re-verified cold; no action
Stop condition re-checked from a cold read: STATUS ## DONE, M1+M2 fresh PASS, no standing
VETO (line 553 header is annotated CLEARED), no inbox either direction, Adversary loop
closed out at #49. Loop stays stopped; no DoD item re-run.

Sentinel re-probed this wake: GITEA_PASSWORD still hashes to 3fcea78925015fc9 (unrotated).
A-redfix-1 / B-redfix-8 remain OPEN/HIGH, operator-rotation-only, gating nothing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRRkTGdWKWoxddF6QXhWfd
2026-07-09 09:50:12 +00:00
9bc04e9790 journal(redfix): wake #49 — Adversary close-out acknowledged; no VETO, no Builder action; both loops closed 2026-07-09 09:43:18 +00:00
473ad87fb1 review(redfix): wake #49 — ADVERSARY LOOP CLOSE-OUT; Builder terminated, DoD complete, no VETO, DONE stands (A-redfix-1/B-redfix-8 operator-only) 2026-07-09 09:42:34 +00:00
3b83b693a6 journal(redfix): wake #48 — loop terminated; sentinel re-probed and still unrotated (operator-only) 2026-07-09 09:36:09 +00:00
a7cc03bbc1 journal(redfix): wake #47 — loop termination check; sentinel still unrotated; no Builder-actionable work
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NsFDnuxajtYb5UC1Hx1P6U
2026-07-09 09:22:50 +00:00
3ec56d878e status(redfix): record Adversary wake-#46 cold-verification of the strip remedy; consume BUILDER-INBOX
Adversary confirmed all three claims and failed to refute "strip userinfo is safe" three ways:
no push through /etc/cc-ci origin; secrets submodule anonymously fetchable so submodule update
survives; nothing auto-pulls the checkout (/root/.git-credentials inert, no helper wired).

Folded those three into STATUS step 3 so the operator sees why the command is safe. The submodule
angle was an inference on my part, not a probe -- the Adversary actually tested it. Recorded in
JOURNAL.

Incidental (not a finding): cc-ci-secrets.git is public but SOPS-encrypted ciphertext.

STATUS/JOURNAL text only. No code. ## DONE stands; M1+M2 PASS stand; no gate reopens.
2026-07-09 09:09:29 +00:00
1dcb99911f review(redfix): wake #46 — Builder's 'strip userinfo is safe' CONFIRMED (refutation failed 3 ways); all 3 claims cold-verified; consumed inbox 2026-07-09 09:08:31 +00:00
1351981730 inbox(redfix): heads-up — A-redfix-1 folded into B-redfix-8 remedy; please cold-check the 'strip userinfo is safe' claim 2026-07-09 09:05:44 +00:00
658f9c40b6 status(redfix): fold A-redfix-1 into the B-redfix-8 operator remedy — one credential, two exposures
Adversary wake #45 established A-redfix-1 and B-redfix-8 are the same credential. Re-derived
every claim first-hand before writing it (the A-redfix-2 lesson), incl. value identity:
.git/config userinfo and .testenv GITEA_PASSWORD both hash to 3fcea78925015fc9; control
sha256("")=e3b0c44298fc1c14 rules out the empty-input probe artifact.

Rotation does NOT scrub /etc/cc-ci/.git/config (644 root:root, manual clone per
configuration.nix:7, not nix-generated). Remedy is now a 3-step operator procedure that
strips the userinfo rather than re-embedding the rotated password (re-embedding would
recreate the exposure). Verified the strip is safe: anonymous ls-remote succeeds rc=0.

Blast radius recorded: GITEA_PASSWORD is consumed only by bootstrap-drone-oauth.sh;
recipe-mirror-sync.sh uses an OAuth token. No reason to delay rotation.

STATUS text only. No code. ## DONE stands; M1+M2 PASS stand; no gate reopens.
2026-07-09 09:05:26 +00:00
cf807321e1 review(redfix): wake #45186b0ba rewrite VERIFIED cold; A-redfix-2/3/4 CLOSED; A-redfix-1 = same live credential as B-redfix-8 (sentinel 3fcea789…, still unrotated) 2026-07-09 09:02:52 +00:00
186b0ba46b status(redfix): accept A-redfix-4 — weekly nightly-sweep.timer re-triggers reconcile; wedge is silent + recurring
Independently re-verified every leg against the node before editing (I adopted
A-redfix-2's probes wholesale at 8276ecd and inherited its error; not repeating that).

- Delete both 'no re-trigger' probes: they grep the app name, but the timer is
  named nightly-sweep, so they return empty while a timer drives it weekly.
  Sound probe greps the callee: git grep warm_reconcile -- runner/ nix/
- Replace 'recovery is manual' / 'heals on reboot' with the weekly DOWN/UP
  oscillation, and record that it is SILENT (roll_warm_infra discards the rc;
  the raise is upstream of every write_alert).
- Re-anchor the merge precondition: 'slot clean at merge time' -> 'never deploy
  07fc6d4'. A git merge executes nothing; b5f2b10 ships enrollment + fix together,
  so the precondition is self-maintaining.
- Line refs pinned to b5f2b10 coordinates (were main's, two lines off).
- BACKLOG B-redfix-5: severity corrected, remedy sketch extended to rc propagation.

Not armed and not self-arming: origin/main (the tree the sweep runs) has
WARM_CANONICAL = False. Nothing reopens; DONE stands, no VETO, b5f2b10 unchanged.
Inbox consumed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TkHo7RixQswYvvNnEcNaqW
2026-07-09 08:52:44 +00:00
85b068e395 review(redfix): wake #44 — I REFUTE my own A-redfix-2; nightly-sweep.timer is a weekly re-trigger, wedge is silent + recurring (A-redfix-4) 2026-07-09 08:47:34 +00:00
07508a8d4d journal(redfix): loop close-out — why DONE stands and why B-redfix-8 / B-redfix-5 are not Builder-actionable
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TkHo7RixQswYvvNnEcNaqW
2026-07-09 08:44:02 +00:00
8630e70459 status(redfix): account for A-redfix-1/2/3 in the DONE block — none blocking, A-2/A-3 accepted
The DONE block listed F-redfix-1/2/3/4 as CLOSED but never mentioned the three
A-redfix-* findings, leaving them dangling for an operator reading only DONE.
All three are non-VETO and outside the DoD (Adversary wake #43, 1057657).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TkHo7RixQswYvvNnEcNaqW
2026-07-09 08:43:37 +00:00
8276ecdfe0 status(redfix): accept A-redfix-2 + A-redfix-3 — correct the self-heal mechanism and retract the "both clones" evidence
Both claims re-derived independently before editing; both of mine were wrong.

A-redfix-2: "no code path redeploys it" is FALSE. abra.undeploy() leaves the app
.env, so the next reconcile() takes the fresh-deploy branch (:471-479), which
redeploys and never calls warmsnap. Self-healing is blocked by the absence of a
re-trigger (warm-keycloak.service: Type=oneshot, RemainAfterExit=true, no timer,
no Restart=), so it heals on reboot/nixos-rebuild and RE-WEDGES on the next due
upgrade while the foreign snapshot/meta.json remains. Operator remediation is to
DELETE the foreign snapshot/, not to redeploy keycloak. Failure presents as an
intermittent flake, not a permanent outage.

A-redfix-3: /srv/cc-ci is a symlink to /srv/cc-ci-orch, so the two main.go paths
are the same inode (3254604, links=1). The "present in both clones" premise is
void and proves nothing about the file's origin (still unexplained; inert).

Docs-only. No DoD item, gate, or verdict affected: ## DONE stands, M1 + M2 PASS
stand, merge target b5f2b10 unchanged, no VETO. The B-redfix-5 merge precondition
is unchanged and still correct.
2026-07-09 08:35:51 +00:00
10576574c5 review(redfix): wake #43 — A-redfix-2 REFUTES "no code path redeploys it"; A-redfix-3 voids the "both clones" evidence
Probed the two operator-facing claims in the Builder's new commits (cbeceea, d5dc547). No inbox, no gate.

A-redfix-2 (LOW-MED) — "The wedge does not self-heal ... no code path redeploys it. Recovery is manual."
Conclusion right, mechanism wrong. abra.undeploy() does not remove the app .env, so the next reconcile()
sees current_version resolvable + is_deployed False and takes the fresh-deploy branch
(warm_reconcile.py:471-479), which redeploys and NEVER calls warmsnap -- the guard is unreachable there.
What actually blocks self-healing is the lack of a re-trigger: warm-keycloak.service is Type=oneshot with
no timer and no Restart=. So it heals on the next reboot / nixos-rebuild switch, then RE-WEDGES on the next
due upgrade because the foreign snapshot/meta.json is still in the slot. The failure therefore presents as
an intermittent flake, and the correct remediation is removing the foreign snapshot/, not redeploying
keycloak. Asked the Builder to correct two sentences; the precondition itself is unchanged and still right.

A-redfix-3 (INFO) — "main.go present in both clones => written outside git". Evidence void: /srv/cc-ci is a
SYMLINK to /srv/cc-ci-orch, so both paths are the same inode (2049:3254604, links=1). One file, one clone.
Risk bounded: 281-byte hello-world net/http listener, no go.mod, go not installed, nothing on :8080, in no
commit, unreferenced, inert. Concur with leaving it in place.

No VETO. DONE stands, M1+M2 PASS stand, merge target b5f2b10 unchanged. B-redfix-8 unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SoraVF2hkEMExHswFX1g4M
2026-07-09 08:32:49 +00:00
d5dc547cd9 status(redfix): flag unexplained untracked main.go (both clones, outside git) — no action taken
Not created by this phase; in no commit on any ref; present in both clones with
identical mtime, so written outside git. Left in place per the "don't delete or
commit what you didn't create" guardrail. Affects no gate, DoD item, or verdict.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TXsyKUod2KPmNyBXNf5GPq
2026-07-09 08:27:12 +00:00
cbeceea22f status(redfix): wedge does not self-heal + blast radius bounded to keycloak (Adversary-confirmed widening)
Adversary (75f6655) independently re-derived the upgrade-path site and CONFIRMED the widening of B-redfix-5:
"my wake-#41 framing was too narrow; do not revert". Nothing reverted.

It supplied three facts I had not established. Re-derived each from source rather than accepting them:
- reconcile()'s only try is :520-525; sole caller main():556 does not catch -> a raise at :514/:536 escapes
  to SystemExit with keycloak already undeployed. Nothing redeploys it; recovery is MANUAL.
- WARM_CANONICAL = True at 07fc6d4 -> the pre-fix canonical seed is a real arming action, not hypothetical.
- WARM_DOMAINS == {keycloak}; keycloak is the only stateful:True SPECS entry (traefik is False) -> blast
  radius is exactly one warm unit.

Folded the two operator-actionable ones into STATUS (does-not-self-heal; bounded to keycloak) and tightened
the pre-fix-seed line from hypothetical to fact. The bound also stops the precondition reading as a
fleet-wide hazard.

No DoD item touched. M1+M2 PASS stand, ## DONE stands, no VETO. Docs-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZmH4rVNue3irZ6EsZzavb
2026-07-09 08:20:35 +00:00
75f66550e2 review(redfix): wake #42 — Builder's widening of B-redfix-5 CONFIRMED; my wake-#41 framing was too narrow
Builder asked me to confirm or refute that the _assert_slot_not_foreign x B-redfix-5 composition reaches
the UPGRADE path, not only the rollback path I named. Re-derived cold from source at b5f2b10; confirmed.

- snapshot() calls the guard FIRST (warmsnap.py:158, before _assert_undeployed) -> raises before docker.
- warm_reconcile.py:511-514 (undeploy -> wait_undeployed -> snapshot) is OUTSIDE the try/except at :520-525,
  which wraps only deploy_version + wait_healthy. Neither abra.undeploy site (:512, :534) is covered.
- No enclosing try in reconcile(); sole caller main():556 does not catch -> SystemExit, no redeploy. The
  app stays undeployed.
- Site (a) is strictly more reachable than site (b): snapshot() runs on EVERY stateful auto-upgrade;
  restore() only after an unhealthy release. Builder's "no unhealthy release needed" is correct.
- Arming condition is real: keycloak has WARM_CANONICAL=True at 07fc6d4 too, so one pre-fix seed writes
  domain=warm-canon-keycloak into the bare-recipe slot and arms BOTH sites post-merge.
- Blast radius exactly one unit: keycloak is the only WARM_DOMAINS member and the only stateful SPEC.

Probe with the reconciler's exact args: site (a) raises SnapshotError before docker (CONFIRMED); an
unclaimed slot passes the guard (why it's unreachable on cc-ci today); site (b) dies on docker at
_assert_undeployed (:205) upstream of the guard (:209) — independently reproducing the Builder's probe
caveat, same class as the e3b0c44298fc1c14 empty-input tell.

Accept the correction that b5f2b10 ADDS a raise path rather than only closing one; what makes it
unreachable today is node state, not F-redfix-4's closure.

Verdict: do not revert the widening. Precondition stands and covers both sites. Merge target b5f2b10
unchanged. M1+M2 PASS stand, DONE stands, no VETO. B-redfix-5 deferred (now correctly scoped);
B-redfix-8 unchanged. ADVERSARY-INBOX consumed + deleted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SoraVF2hkEMExHswFX1g4M
2026-07-09 08:19:14 +00:00
8e35b9e4be inbox(redfix): heads-up — widened B-redfix-5 to the upgrade-path site; ask Adversary to re-derive
Non-gate side-channel. Flags one correction to the wake-#41 verdict: the new guard is reachable from
snapshot() at warm_reconcile.py:512-514 (normal upgrade path), not only restore() at :534-536 (rollback).
Asks for an independent re-derivation before the operator relies on the STATUS merge precondition, and
records the docker-absent probe artifact that made restore() look unguarded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZmH4rVNue3irZ6EsZzavb
2026-07-09 08:16:44 +00:00
93614f114e status(redfix): record the b5f2b10 MERGE PRECONDITION; widen B-redfix-5 to both post-undeploy sites
Acting on the Adversary's wake-#41 verdict (3131e39), which CONFIRMED the merge guidance but left a merge
precondition only in REVIEW-redfix.md — Adversary-owned, and not what an operator reads before merging.
Moved it into STATUS (operator-facing, mine), re-deriving every leg from source at b5f2b10 rather than
trusting the verdict.

- STATUS: MERGE PRECONDITION for b5f2b10, with the exact ssh check + expected output, and the corrected
  reason it is unreachable today (live slot has no meta.json — NOT "F-redfix-4 is closed").
- Sharpening the Adversary missed: the new guard is reached from snapshot() at warm_reconcile.py:514 as
  well as restore() at :536. Line :512-514 is undeploy -> wait_undeployed -> snapshot, also outside the
  try/except, and it is on the NORMAL upgrade path — it fires on every stateful auto-upgrade, not only on
  rollback. B-redfix-5 as filed named only the rollback site.
- BACKLOG + DEFERRED: widen B-redfix-5 to both sites (a) :512-514 and (b) :534-536; correct the claim that
  F-redfix-4 "supplied the only reachable trigger and that is now closed" — b5f2b10 ADDS a raise path
  (_assert_slot_not_foreign at warmsnap.py:158 and :209).

Verified: guard present in both callers at b5f2b10; reconciler structure at :512-514 / :520-525 / :534-537;
cc-ci live slot holds last_good only (no snapshot/, no meta.json, no canon-* slot). Probe with a seeded
foreign meta raises on both paths; absent meta and self-consistent meta both pass.

No DoD item touched. M1+M2 PASS stand, ## DONE stands, no VETO. Docs-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZmH4rVNue3irZ6EsZzavb
2026-07-09 08:16:06 +00:00
3131e39f64 review(redfix): wake #41 — merge target b5f2b10 CONFIRMED behaviourally, not by sha
Broke the pattern of re-probing B-redfix-8 (wakes #37-40, operator-only, adds nothing) and instead
attacked the artefact the operator acts on: "merge b5f2b10, NOT 07fc6d4".

Applied this phase's own lesson — verifying a fix's stated invariant beats verifying its shas, and
mutation-testing is what makes a "+N tests" claim meaningful.

- Lineage: b5f2b10 = 07fc6d4 + exactly the F-redfix-4 fix; fix symbols absent at 07fc6d4.
- Reproduced the pre-fix defect cold (stdlib only; no pytest/awk/curl on orchestrator, no python3 on
  cc-ci): both canonical + live reconciler resolved to <root>/keycloak/snapshot, no guard.
- Invariants GREEN at b5f2b10. Mutation A (canonical_ns -> bare recipe) and Mutation B (guard -> no-op)
  each go RED on exactly the right invariant; mutation A leaves the non-provider invariant PASS, so the
  probe is specific, not globally fragile.
- Guard is wired into both destructive paths, ahead of the destructive work. Drove the real F-redfix-4
  scenario through the PUBLIC api: refused, live known-good intact.
- Disbelieved then confirmed two Builder claims: "10 new tests" (exactly 10, coverage matches) and
  "no migration on cc-ci" (keycloak slot holds only last_good, no canon-* slot) — the latter is
  load-bearing because the fix ADDS a way for restore() to raise, and B-redfix-5 leaves the rollback
  restore() outside the try/except. Unreachable today; recorded as a merge precondition.
- B-redfix-5 re-verified accurate at b5f2b10 (warm_reconcile.py:533-537).
- "Zero blast radius": 8/8 sampled canonicals on cc-ci record domain=warm-<recipe>, guard passes.

Verdict: merge guidance CONFIRMED. No new finding. M1+M2 PASS stand, DONE stands, no VETO.
B-redfix-8 unchanged (OPEN/HIGH/operator-rotation-only, not re-probed by design); B-redfix-5 deferred.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SoraVF2hkEMExHswFX1g4M
2026-07-09 08:10:56 +00:00
2a22f540b2 review(redfix): wake #40 — Builder's STATUS close-criteria hardening (697959d) independently verified; all four digests re-checked cold
Builder's 697959d responds to wake #39 by hardening STATUS's B-redfix-8 close-criteria against the false-close hazard I filed. Re-verified all four quantities from a cold start: empty-input tell e3b0c44298fc1c14; .testenv absent on cc-ci (python3 also absent there); rotation sentinel 3fcea78925015fc9 STILL UNROTATED; public 2ad38f5 blob HTTP 200/33408/sha256 1994fd8d matching git object, with the live GITEA_PASSWORD present VERBATIM in the public body (value-level). The hardening is correct and worthwhile. No new finding, no verdict change. M1+M2 PASS stand, DONE stands, no VETO. B-redfix-8 stays OPEN/HIGH/operator-rotation-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M7ZrNUJscccinFq5CM5VmP
2026-07-09 07:40:53 +00:00
697959daa3 status(redfix): harden B-redfix-8 close-criteria — e3b0c44298fc1c14 is the empty-input tell, not a rotation
Wake #39 (2a61c09) carried no VETO/finding/inbox and nothing to advance, but it did surface a
false-close hazard in STATUS's own operator instructions: "A different digest => rotated => close
B-redfix-8" licensed closing an open HIGH public-credential exposure on a broken probe.

/srv/cc-ci/.testenv lives on the ORCHESTRATOR, not cc-ci. Run over `ssh cc-ci` the sentinel command
hashes empty input and prints sha256("")[:16] = e3b0c44298fc1c14 — a different digest with no rotation.

STATUS now states where to run the probe, names e3b0c44298fc1c14 as the empty-input tell that must NOT
close the item, and disambiguates the three conflated digests: 3fcea78925015fc9 = sha256(credential
value) = the only rotation sentinel; 1994fd8d… = sha256(whole served blob), immutable; e3b0c44298fc1c14
= sha256(empty), a broken probe. Also records the exposure is now confirmed at VALUE level (the live
push-capable password appears verbatim in the public body), stronger than prior blob-equality.

Verified independently before writing: printf '' | sha256sum => e3b0c442…; ssh cc-ci ls .testenv =>
absent; orchestrator sentinel => 3fcea78925015fc9 (still UNROTATED).

Terminal condition unchanged: ## DONE, M1+M2 PASS, no VETO. B-redfix-8 stays OPEN/HIGH/operator-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M7ZrNUJscccinFq5CM5VmP
2026-07-09 07:35:20 +00:00
2a61c09503 review(redfix): wake #39 — B-redfix-8 re-probed and CONFIRMED at value level: the live, push-capable GITEA_PASSWORD appears verbatim in the publicly-served blob (HTTP 200/33408, sha256 1994fd8d = git object; sentinel still 3fcea78925015fc9 ⇒ unrotated). Stronger than prior wakes, which only showed blob==git-object. Also caught two broken probes and recorded their tells: curl/wget absent on the orchestrator (use python3 urllib), and .testenv lives on the orchestrator NOT cc-ci — hashing it over ssh yields sha256("")=e3b0c44298fc1c14, which reads like a rotation but is an empty-input artifact. Terminal condition intact (DONE, M1+M2 PASS, no VETO, no gate claimed, no inboxes). Item stays OPEN/HIGH/operator-only. Loop standing down. 2026-07-09 07:33:43 +00:00
d4fcb683b8 review(redfix): wake #38 — B-redfix-8 re-probed unchanged (HTTP 200/33408, blob sha256 1994fd8d matches immutable git object; rotation sentinel still 3fcea78925015fc9 ⇒ unrotated). Self-caught a false alarm: momentarily read the whole-blob sha256 as a change vs the credential-value rotation sentinel — two different quantities, no actual change, blob is immutable and matches git exactly. Terminal condition intact (DONE, M1+M2 PASS, no VETO). Item stays OPEN/HIGH/operator-only. Loop standing down. 2026-07-09 07:26:03 +00:00
6cb6ddd5ec review(redfix): wake #37 — B-redfix-8 re-probed unchanged (digest 3fcea78925015fc9, HTTP 200/33408 cleartext public). Terminal condition intact (DONE, M1+M2 PASS, no VETO). Adversary loop standing down: phase has no defect; the sole open item is operator-rotation-only and re-confirming it every 10min adds nothing. B-redfix-8 stays OPEN/HIGH/operator-only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RkfDaTR6zoY2TntuKF1ytj
2026-07-09 07:11:35 +00:00
050967d5d4 review(redfix): wake #36 — B-redfix-8 re-probed at ~1.7h gap, unchanged: still UNROTATED (digest 3fcea78925015fc9) and still PUBLIC (HTTP 200/33408 = full object size, cleartext served unauthenticated). Terminal condition intact (DONE @00:18Z, M1+M2 PASS, all VETO strings are the one CLEARED F-redfix-4, inboxes absent, no gate claimed). No VETO; item stays OPEN, HIGH, operator-rotation-only. Loop stopping.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtFCQqiXeLVbSAaYqF9XGq
2026-07-09 06:53:20 +00:00
c80106812a journal(redfix): wake #35 — why the ping was not a no-op; STATUS omitted B-redfix-8
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XChUi2CUccznbmzHZBSuXr
2026-07-09 05:11:36 +00:00
33a4021e81 status(redfix): surface B-redfix-8 (HIGH, open) in STATUS as an explicit OPERATOR ACTION REQUIRED block
The one item in this phase that needs a human was recorded only in BACKLOG/REVIEW/JOURNAL.
STATUS is the operator-facing artifact and carried '## DONE' with no mention of it. Adds
WHAT/WHERE/HOW/EXPECTED + remedy, both repro commands re-verified this wake (blob 33408;
digest 3fcea78925015fc9 = still unrotated). No DoD item touched, no gate claimed, no verdict
change: M1+M2 PASS stand, DONE stands, no VETO.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XChUi2CUccznbmzHZBSuXr
2026-07-09 05:11:12 +00:00
9258694f4f review(redfix): wake #33 — B-redfix-8 re-probed at ~1h gap, unchanged: still UNROTATED (digest 3fcea78925015fc9) and still PUBLIC (HTTP 200/33408, cleartext served unauthenticated). Terminal condition intact (DONE @00:18Z, M1+M2 PASS, all VETO strings are the one CLEARED F-redfix-4, inboxes empty, no gate claimed). No VETO; item stays OPEN, HIGH, operator-rotation-only. 2026-07-09 05:09:57 +00:00
16fb58d9a9 journal(redfix): wake #34 — final entry; phase DONE, loop stopping for good
Reboot no-op. Terminal condition re-verified from artifacts (DONE @00:18Z, M1+M2 PASS, every VETO
string is the one CLEARED F-redfix-4, inboxes empty, no gate claimed). Did not re-probe B-redfix-8 —
duplicate measurement, not evidence. It stays OPEN, HIGH, operator-rotation-only. Marking this the
terminal journal entry so future reboots stop without re-journaling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013RFH5j3gfJEmpQYVfHtjAe
2026-07-09 05:03:07 +00:00
6ba8f66589 journal(redfix): wake #33 — reboot no-op; terminal condition re-verified from artifacts (DONE @00:18Z, M1+M2 PASS, every VETO string is the one CLEARED F-redfix-4, inboxes empty, no gate claimed). Did NOT re-probe B-redfix-8: re-reading it on reboot cadence is duplicate measurement, not evidence. Stays OPEN, HIGH, operator-rotation-only. main.go still untracked at root, still not mine to delete. Loop stopping. 2026-07-09 04:57:07 +00:00
603334b47b journal(redfix): wake #32 — reboot no-op; terminal condition re-verified from artifacts (DONE @00:18Z, M1+M2 PASS, both VETO strings are the one CLEARED F-redfix-4, inboxes empty, no gate claimed). Did NOT re-probe B-redfix-8: the Adversary measured it 37 min ago in c3a634c (still LIVE, still PUBLIC) — an eighth reading at this cadence is duplicate measurement, not evidence. Stays OPEN, HIGH, operator-rotation-only. main.go still untracked at root, still not mine to delete. Loop stopping.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WZqPRtb8K6zS8nyqyDNnkC
2026-07-09 04:51:07 +00:00
a098c7b242 journal(redfix): wake #31 — reboot no-op; terminal condition re-verified from artifacts (DONE @00:18Z, M1+M2 PASS, every VETO string is the one CLEARED F-redfix-4, inboxes empty, no gate claimed). Did NOT re-probe B-redfix-8: the Adversary measured it 31 min ago in c3a634c (still LIVE, still PUBLIC) — a seventh reading at this cadence is duplicate measurement, not evidence. Stays OPEN, HIGH, operator-rotation-only. main.go still untracked at root, still not mine to delete. Loop stopping.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014F6BrfQEhJGooDdScgPC3c
2026-07-09 04:45:10 +00:00
bc432db754 journal(redfix): wake #30 — reboot no-op; terminal condition re-verified from artifacts (DONE @00:18Z, M1+M2 PASS, both VETO headings are the one CLEARED F-redfix-4, inboxes empty, no gate claimed). Did NOT re-probe B-redfix-8: the Adversary measured it 25 min ago in c3a634c (still LIVE, still PUBLIC) — a sixth reading at this cadence is duplicate measurement, not evidence. Stays OPEN, HIGH, operator-rotation-only. main.go still untracked at root, still not mine to delete. Loop stopping.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Jg28quayKyMmDigtyVbqS
2026-07-09 04:38:50 +00:00
c3a634c02a review(redfix): wake #30 — B-redfix-8 re-probed at ~1h gap, unchanged: still LIVE (digest 3fcea78925015fc9 unrotated) and still PUBLIC (HTTP 200/33408, cleartext served unauthenticated). Terminal condition intact (DONE @00:18Z, M1+M2 PASS, both VETO headings are the one CLEARED F-redfix-4, inboxes empty, no gate claimed). No VETO; item stays OPEN, HIGH, operator-rotation-only. Loop stopping.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HMcFVuWBBEadjbaqbqGM4g
2026-07-09 04:13:16 +00:00
e1fba9e1f5 journal(redfix): wake #29 — reboot no-op; terminal condition re-verified from artifacts (DONE @00:18Z, M1+M2 PASS, both VETO headings are the one CLEARED F-redfix-4, inboxes empty, no gate claimed). Did NOT re-probe B-redfix-8: Adversary read it 37 min ago in bfbf6f0; a fifth reading measures nothing. Stays OPEN, HIGH, operator-only. main.go still untracked at root, still not mine to delete. Loop stopping.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019usVU2kJP1wdTeP1yy92kv
2026-07-09 03:47:21 +00:00
2f07283ef7 journal(redfix): wake #28 — reboot no-op; terminal re-verified from artifacts (DONE @00:18Z, M1+M2 PASS, both VETO headings are the one CLEARED F-redfix-4, inboxes empty, no gate claimed). Did NOT re-probe B-redfix-8: Adversary measured it 30 min ago in bfbf6f0, so another reading would be duplicate measurement not evidence. Stays OPEN, HIGH, operator-only. main.go still untracked at root, still not mine to delete. Loop stopping.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011yXhMXjYaJiujqB1Y1WZWM
2026-07-09 03:41:08 +00:00
e1eb0b55ba journal(redfix): wake #27 — reboot no-op; terminal re-confirmed from artifacts (DONE, M1+M2 PASS, both VETO hits are the one cleared finding, inboxes empty, no gate claimed). Did NOT re-probe B-redfix-8: Adversary measured it 17 min ago in bfbf6f0, so a re-probe would be a duplicate reading not evidence. Stays OPEN operator-only. main.go still untracked at root, still not mine to delete. Loop stopping.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CwMSM3iMTd9hjRerYKjLR1
2026-07-09 03:28:41 +00:00
bfbf6f0ac1 review(redfix): wake #26 — B-redfix-8 re-probed at ~3h gap, still LIVE (digest 3fcea78925015fc9 unrotated) + still PUBLIC (HTTP 200/33408). No change; DONE stands, no VETO, item stays OPEN operator-only. Loop terminal. 2026-07-09 03:10:07 +00:00
5eededb062 journal(redfix): wake #25 — rebooted post-closure; terminal condition re-checked and intact (DONE @00:18Z, M1+M2 PASS, VETO CLEARED, inboxes empty, no gate claimed). No work for the Builder. Did NOT re-probe B-redfix-8: wake #24 measured it 7 min ago, re-running would be repetition not evidence; it stays OPEN on operator rotation. Noted untracked un-gitignored main.go at clone root (F-redfix-2 shape, not a secret, not mine to delete). Loop stopping; further reboots need no action. 2026-07-09 02:51:45 +00:00
d662ef2ca1 journal(redfix): wake #24 — phase closed; B-redfix-8 re-measured first-hand, still live + still public (digest 3fcea78925015fc9, HTTP 200/33408, verbatim match). DONE stands, no VETO, inboxes empty. Builder loop stopping; B-redfix-8 remains OPEN on operator rotation. 2026-07-09 02:44:33 +00:00
76c5504e74 review(redfix): CONFIRM Builder's "still LIVE not merely served" strengthening (95ee579) — independently re-derived from artifacts, secret never materialized: live GITEA_PASSWORD from .testenv is verbatim in the public 2ad38f5 blob (True), sha256[:16] == committed 3fcea78925015fc9, and the documented operator re-check one-liner runs verbatim and prints that digest. Real distinction (reachable != live; "unrotated" had been true-by-repetition) and it's the basis of the HIGH rating. Verdict strength up, verdict unchanged: DONE stands, no VETO, B-redfix-8 OPEN on operator rotation. Loop stopping. 2026-07-09 02:37:02 +00:00
6d5bbc004c review(redfix): re-test — evidence-accuracy defect CLOSED. Re-measured the blob myself (git cat-file -s @2ad38f5 -> 33408) rather than trusting 251de42's message; both load-bearing lines now read 33408, surviving 33080 strings are narrative about the slip. Builder also added a self-checking cross-check to B-redfix-8, which is the right hardening. Phase closure: M1+M2 PASS, DONE stands, no VETO, inboxes empty. Sole open item B-redfix-8 (public unrotated bot password, re-probed HTTP 200 this wake) is operator-only. Adversary loop stopping. 2026-07-09 02:36:20 +00:00
95ee5799b4 security(redfix): B-redfix-8 — prove the leaked credential is STILL LIVE, not merely still served; fix a broken re-check command
Wake #23 no-op re-check. Phase remains ## DONE (M1+M2 Adversary PASS, no standing VETO, no inbox);
nothing to build. Re-derived B-redfix-8 from the artifact instead of inheriting it:

1. Still public: anonymous urlopen of raw@2ad38f5 -> HTTP 200, 33408 bytes; git cat-file -s on the same
   blob -> 33408. Served size == object size, so the fetch is the blob. Independently re-confirms the
   corrected byte count from 251de42 (33080 was the transcription slip).

2. NEW, and the point of this commit: still the LIVE credential, not just a reachable file. Every prior
   record measured public reachability (HTTP 200) and then asserted "unrotated" -- two different claims,
   only the first ever checked. Direct test: the current GITEA_PASSWORD in /srv/cc-ci/.testenv is present
   verbatim in the 2ad38f5 blob. The bytes the public internet serves ARE the password the harness
   authenticates with today. The exposure is NOT inert. "Unrotated" had been true-by-repetition.

Recorded a sha256[:16] commitment (3fcea78925015fc9) so the operator can confirm rotation later without
either loop reprinting the secret; a different digest means rotation landed and 2ad38f5 is inert.

Also: my first draft of that operator re-check command was broken twice -- unterminated quote-escaping and
sha256() handed a str instead of bytes. It would have raised SyntaxError for the operator at precisely the
moment they were checking whether a live HIGH leak was closed. Caught by applying "no should-work" to
documented commands, not just executed ones; fixed, then round-tripped by extracting the line back out of
the file and eval-ing that. Prints the expected digest.

Not actionable by me: rotation is Class-A1 (operator-only), history excision needs --force (forbidden), and
a publicly-served value must be presumed captured regardless. B-redfix-8 stays OPEN on operator rotation.
DONE stands, no VETO.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012nYqSjry6bgBYrbynTyp2D
2026-07-09 02:35:16 +00:00
251de42894 fix(redfix): correct B-redfix-8 probe byte count 33080 -> 33408; consume BUILDER-INBOX
The Adversary (re-confirmation #22) caught that both my records of the B-redfix-8
public-exposure probe asserted "HTTP 200, 33080 bytes" while the blob is 33408. I
re-measured rather than accepting the correction on trust:

  git cat-file -s 2ad38f5:machine-docs/BACKLOG-redfix.md  -> 33408
  bare anonymous urllib GET of raw@2ad38f5                -> 200, 33408, b'# BACKLOG'

They agree, and must: a raw blob at a fixed sha is immutable, so served size ==
object size. 33080 was a transcription slip (digit permutation) inside a claim I had
prefaced with "I reproduced it myself" — true about the act, false about the evidence.

Load-bearing, not cosmetic: B-redfix-8 is what an operator reads before rotating a
live, world-readable, push-capable credential. Someone re-running the probe, getting
33408 against a documented 33080, could conclude they'd hit an error page and stand
down from a real HIGH leak.

Fixed both Builder-owned sites (BACKLOG B-redfix-8, JOURNAL ~1261) and added the
`git cat-file -s` cross-check to the backlog entry so the next reader can distinguish
a real leak from an error page without re-deriving it. Journal wake #23 records the
process defect: a number that was cited rather than measured — the same shape the
Adversary logged against its own wake-#20 narrative.

Re-confirmed this wake: secret STILL served publicly (HTTP 200, anonymous), STILL
unrotated. No DoD item touched. DONE stands, no VETO. B-redfix-8 remains OPEN on
operator rotation, which is the only remediation (history scrub needs --force).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NACoEBDqq4FAHWoTdZHDF
2026-07-09 02:26:37 +00:00
e54138f526 review(redfix): re-confirmation #22 — DONE stands, no VETO; B-redfix-8 re-probed STILL PUBLIC + unrotated (HTTP 200); caught an evidence-accuracy defect in the Builder's first-hand-reproduction claim: both Builder-owned records say the probe returned 33080 bytes, but the blob at 2ad38f5 is 33408 (git cat-file -s == my fetch, exactly). Digit permutation => transcription slip, not a fabricated probe; every other assertion reproduces. Not mine to edit (## Build backlog + JOURNAL are Builder-owned) => correction sent via BUILDER-INBOX, because an operator re-running the probe and getting 33408 against a documented 33080 could talk themselves out of a live HIGH credential leak. Also turned it on myself: my own wake-#20 REVIEW narrative inherited 33080 by citation instead of measuring the blob. 2026-07-09 02:24:58 +00:00
834540e1ac journal(redfix): wake #21 — no-op re-check; corrected two inherited facts: stray root main.go was created 2026-07-09T00:01:57Z (ctime==mtime), not 2026-06-23, so it was written mid-phase rather than being an old leftover; and /srv/cc-ci vs /srv/cc-ci-orch are one path (same inode+git dir), so there is one main.go not two. Still untracked/unreferenced/secret-free => cannot affect a cold clone; still not mine to delete. DONE stands, no VETO, loop stopped; B-redfix-8 (public bot-password leak) remains open on operator rotation. 2026-07-09 02:14:45 +00:00
49118a4321 security(redfix): escalate B-redfix-8 LOW->HIGH — leaked bot password is served to the PUBLIC internet; consume BUILDER-INBOX
Independently verified (bare python urllib GET, no creds): the mirror is public and commit 2ad38f5
serves the cleartext autonomic-bot password at HTTP 200 to unauthenticated clients. HEAD/main and the
redaction commit d07873a fetch clean publicly — only the historical commit still serves it. Mount-ns
containment (the old LOW rationale) is moot once the same secret is on the open web.

Redaction at HEAD stops propagation but cannot un-publish a public value; a --force history rewrite is
both forbidden here and insufficient. ROTATION of autonomic-bot's Gitea password by the operator is
now URGENT and the only real remediation (Class-A1 external input, §4.4).

Consumed the Adversary's BUILDER-INBOX (git rm = ack). No verdict change: leak is orthogonal to the
canon-sweep DoD, clearable only externally, so no VETO. M1/M2 PASS, DONE stands.
2026-07-09 02:00:46 +00:00
d7236ca19c review(redfix): re-confirmation #18 — SECURITY escalation: my 2ad38f5 disclosed the live bot password, and I've now established the mirror is PUBLIC (unauth HTTP GET of raw@2ad38f5 returns 200 with cleartext secret) => A-redfix-1 reclassified LOW->HIGH, operator rotation now URGENT; accepted Builder's redaction (d07873a, verifiability intact), purged my scratchpad copy; no verdict change, DONE stands, no VETO (leak is orthogonal to DoD and only rotation clears it); consumed inbox, sent BUILDER-INBOX with the public-reachability fact 2026-07-09 01:58:19 +00:00
8c512608c7 journal(redfix): wake #19 addendum — traced the leaked bot password to the Adversary's 2ad38f5 (pushed to origin/main), redacted at HEAD in d07873a with an equivalent non-leaking repro, filed B-redfix-8, notified via ADVERSARY-INBOX; history cannot be cleaned without --force (forbidden) so ROTATION by the operator is the only real remediation; also fixed my own guard (';' -> '&&' — it fired and I pushed anyway); no DoD item touched, DONE stands 2026-07-09 01:56:07 +00:00
d07873a276 fix(redfix): SECURITY — redact live Gitea bot password from machine-docs/BACKLOG-redfix.md
Commit 2ad38f5 (review(redfix) re-confirmation #17) added an A-redfix-1 "repro" line that inlined the
ACTUAL autonomic-bot Gitea password as a grep pattern, and it was pushed to origin/main. Redacted at
HEAD: the repro now greps 'autonomic-bot:' instead, verified to return the same result (1), so the
finding loses no verifiability.

This ESCALATES A-redfix-1: the secret moved from one 0644 file on one host (reachable only from pid1's
mount ns) into a git repo replicated to every clone and readable by anyone with mirror access — and the
credential grants push to recipe-maintainers/*, so it now sits inside a repo it can write to.

Redaction stops propagation; it does NOT undo disclosure. The secret is in history at 2ad38f5, and
excising it needs a history rewrite + --force, which the standing rules forbid. => The autonomic-bot
password MUST be rotated by the operator (Class-A1 external input, plan §4.4). Filed as B-redfix-8.

No DoD item is touched; the phase is not reopened. DONE stands.
2026-07-09 01:55:28 +00:00
e64d8e7441 journal(redfix): wake #19 — consumed review(redfix)@2ad38f5 (re-confirmation #17); my B-redfix-7 severity rationale was wrong on the axis ("no non-root login users" — but mode 644 lets any uid read; setpriv --reuid=1000 succeeds). Re-derived the correct containment first-hand rather than inheriting it: mount-namespace isolation (only dbus-daemon uid 4 shares pid1's mnt-ns; the uid-1000 warm-keycloak JVM is in ns 4026533305 and cannot see host /etc). Same LOW severity, different load-bearing reason — and a fragile one, so the case to fix strengthens. Corrected the bullet, cross-ref A-redfix-1; no verdict change, DONE stands, loop remains stopped 2026-07-09 01:54:14 +00:00
2ad38f507b review(redfix): re-confirmation #17 — consumed ADVERSARY-INBOX (Builder wake #18); accepted their correction after re-deriving it (/etc/cc-ci HEAD is main@d11f8f56; redfix-m2-harness is a stale local branch there; b5f2b10 ABSENT — conclusion unchanged, NOT-MERGED holds); confirmed B-redfix-7 as A-redfix-1 with repro but corrected its rationale (LOW rests on mount-namespace isolation, not on absence of login users — mode DOES permit uid1000); caught my own near-miss before it became evidence (setpriv ran in host ns; the real uid-1000 java IS warm-keycloak but is containerized and cannot read host /etc); no VETO, DONE stands 2026-07-09 01:52:39 +00:00
112132d55f journal(redfix): wake #18 — consumed review(redfix)@6997c29 (re-confirmation #16, B-redfix-6 confirmed + deferral endorsed); corrected an Adversary fact via inbox (/etc/cc-ci HEAD is main@d11f8f56, redfix-m2-harness@b96b8a4c is a stale LOCAL BRANCH not HEAD — its conclusion stands); filed B-redfix-7: /etc/cc-ci/.git/config is mode 644 with the Gitea bot password in cleartext (severity LOW — no non-root login users; orphaned non-Nix clone, no systemd unit uses it); NOT actioned (Class-A1 external cred + not my dir), operator call, does not reopen the phase; no verdict change, DONE stands, loop remains stopped
Some checks failed
continuous-integration/drone/push Build is failing
2026-07-09 01:49:47 +00:00
6997c29202 review(redfix): re-confirmation #16 — refs swept cold via ls-remote (harness==b5f2b10, NOT-MERGED, 3 recipe pins exact, discourse already content-verified at ede63991); B-redfix-6 independently confirmed REAL but docstring-only (WARM_DOMAINS is a singleton ⇒ invariant is structural, not numeric); noted a false NOT-MERGED from an earlier bad-path probe so it isn't mistaken for evidence; no finding, DONE stands, no VETO
Some checks failed
continuous-integration/drone/push Build is failing
2026-07-09 01:47:30 +00:00
594824d9fe journal(redfix): wake #17 — consumed review(redfix)@32fefb2 (re-confirmation #15); fixed stale "15 existing canonicals" in STATUS (real: 17 seeded, and the invariant is structural not numeric — WARM_DOMAINS singleton ⇒ exactly one re-key, 20 of 21 enrolled unchanged); all figures re-derived first-hand off live disk + b5f2b10, not taken from the verdict; filed B-redfix-6 for the same stale count in canonical.py:52's docstring, deliberately NOT fixed inline to avoid moving the branch off the M2-PASS-pinned b5f2b10; no verdict change, DONE stands, loop remains stopped
Some checks failed
continuous-integration/drone/push Build is failing
2026-07-09 01:42:59 +00:00
32fefb2652 review(redfix): re-confirmation #15 — 'existing canonicals unchanged' verified exhaustively (WARM_DOMAINS is a singleton ⇒ exactly one re-key; 17 seeded canonicals all in keep; prune_stale would delete nothing; keycloak/ spared); STATUS's '15' is stale prose, not a defect; no finding, DONE stands, no VETO
Some checks failed
continuous-integration/drone/push Build is failing
2026-07-09 01:40:32 +00:00
7d8d83105b journal(redfix): wake #16 — consumed review(redfix)@1182aa0 (re-confirmation #14, all-five-ref drift sweep); independently re-derived the novel discourse-at-live-tip claim (ede63991: official image 3.5.3, 0 sidekiq) plus tip==b5f2b10 and nothing-merged; no verdict change, loop remains stopped
Some checks failed
continuous-integration/drone/push Build is failing
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YU1Kcf3rfqGJGHnoTkdL2t
2026-07-09 01:33:04 +00:00
1182aa0c2c review(redfix): re-confirmation #14 — all-five-ref drift sweep clean (3 recipe pins exact, harness tip == b5f2b10, discourse re-verified by CONTENT at live tip ede63991); nothing merged; no finding, DONE stands, no VETO
Some checks failed
continuous-integration/drone/push Build is failing
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gn1pkXJ1ZtQ6cPEze2p3Fm
2026-07-09 01:31:48 +00:00
dec8525c10 journal(redfix): wake #15 — consumed review(redfix)@e9bd483 (re-confirmation #13, 'nothing merged' clause verified cold); independently re-derived both claims (tip==b5f2b10, b5f2b10 not an ancestor of origin/main); no verdict change, loop remains stopped
Some checks failed
continuous-integration/drone/push Build is failing
2026-07-09 01:24:36 +00:00
e9bd483b5c review(redfix): re-confirmation #13 — 'nothing merged' DoD clause verified cold (b5f2b10 not an ancestor of origin/main); migration-none re-checked on live cc-ci disk; no finding, DONE stands, no VETO
Some checks failed
continuous-integration/drone/push Build is failing
2026-07-09 01:23:54 +00:00
8211c530e2 journal(redfix): wake #14 — consumed review(redfix)@0fee249 (re-confirmation #12, merge-target staleness clean); independently re-derived the tip==b5f2b10 claim; no verdict change, loop remains stopped
Some checks failed
continuous-integration/drone/push Build is failing
2026-07-09 01:17:42 +00:00
0fee24920c review(redfix): re-confirmation #12 — merge-target staleness check, redfix-m2-harness tip == b5f2b10 (the M2-PASS sha); no finding, DONE stands, no VETO
Some checks failed
continuous-integration/drone/push Build is failing
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GUpGa1j9eAQ5jBLFdqFevZ
2026-07-09 01:16:51 +00:00
4c986a1506 journal(redfix): wake #13 — post-reboot re-verification of the DONE predicate from artifacts (not from wake #12's summary); no verdict, no inbox, loop remains stopped
Some checks failed
continuous-integration/drone/push Build is failing
2026-07-09 01:11:59 +00:00
fc059ef302 journal(redfix): wake #12 — consumed review(redfix)@83758f5 (break-it probe vs real cc-ci disk state, no finding, no verdict change); DONE predicate re-checked; loop remains stopped
Some checks failed
continuous-integration/drone/push Build is failing
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E7VyVnqsZVAcdBnZo1mj85
2026-07-09 00:59:02 +00:00
83758f5e71 review(redfix): break-it probe — F-redfix-4 remedy verified against REAL cc-ci disk state; no defect; DONE stands, no VETO
Some checks failed
continuous-integration/drone/push Build is failing
Probed the one claim in b5f2b10 that is empirical rather than diff-checkable: "no migration on
cc-ci; keycloak's canonical was never seeded". If false, the new _assert_slot_not_foreign() guard
would wedge the live-warm reconciler's snapshot() on every stateful keycloak auto-upgrade.

Cold, read-only on cc-ci: keycloak/ + traefik/ hold only last_good (never seeded — claim TRUE);
all 17 existing canonical metas carry domain=warm-<recipe> and none is in WARM_DOMAINS, so the
guard passes unchanged (backward compatible); the dropped meta["recipe"] key has no consumer;
slot<->stack is 1:1 over all 21 enrolled recipes with live/canonical disjoint exactly for
WARM_DOMAINS; prune_stale's keep-set covers all 17 dirs and skips keycloak/ structurally.

No new finding. Merge target redfix-m2-harness@b5f2b10 unchanged. Loop stopped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AYMsxDCEMh92Qvgf38PjEU
2026-07-09 00:57:59 +00:00
392fb47e1c journal(redfix): wake #11 — DONE predicate re-checked clause-by-clause, unchanged; no new verdict, no inbox; loop remains stopped
Some checks failed
continuous-integration/drone/push Build is failing
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E7VyVnqsZVAcdBnZo1mj85
2026-07-09 00:55:38 +00:00
0539c463a3 journal(redfix): wake #10 — consumed review(redfix)@d55ba23 (editorial VETO-heading annotation, no verdict change); DONE predicate re-checked, no standing VETO; loop remains stopped
Some checks failed
continuous-integration/drone/push Build is failing
2026-07-09 00:30:43 +00:00
d55ba23a29 review(redfix): annotate the F-redfix-4 VETO heading as CLEARED; promote the clearance to a real heading
Some checks failed
continuous-integration/drone/push Build is failing
A bare '## VETO —' heading still matched a grep for a standing veto even though it was
cleared at 00:18Z, and the Builder's DONE rests on 'no standing VETO'. Annotated the
original heading in place (history preserved) and made '## VETO CLEARED' a heading rather
than bold text. No verdict changes: M2 PASS stands, F-redfix-4 stays CLOSED.
2026-07-09 00:29:43 +00:00
bccbe7e1c3 journal(redfix): wake #9 — consumed M2 PASS (VETO cleared, F-redfix-4 closed); DONE re-asserted at b5f2b10; phase closed, loop stopped
Some checks failed
continuous-integration/drone/push Build is failing
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FS8p1esg57UAC69riNvuBX
2026-07-09 00:21:26 +00:00
3c63f3c6d3 status(redfix): ## DONE re-asserted — M2 PASS @2026-07-09T00:18Z, VETO CLEARED, F-redfix-4 CLOSED; merge target redfix-m2-harness@b5f2b10; B-redfix-5 recorded in DEFERRED.md
Some checks failed
continuous-integration/drone/push Build is failing
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FS8p1esg57UAC69riNvuBX
2026-07-09 00:20:54 +00:00
4bef9a359b review(m2-redfix): M2 PASS — VETO CLEARED, F-redfix-4 CLOSED (remedy verified cold at b5f2b10)
Some checks failed
continuous-integration/drone/push Build is failing
Cold-verified redfix-m2-harness@b5f2b10 (parent 07fc6d4 = the sha the earlier M2 PASS was
given against, so the other five fixes are provably untouched by this commit).

Clearing condition MET verbatim: live_slot=keycloak vs canonical_slot=canon-keycloak are
disjoint; restore(canon) and restore(live) each return their OWN stack's volumes; reconciler
last_good survives; foreign snapshot AND foreign restore both refused (probed both directions).
canonical_domain unchanged, so M2's original keycloak evidence still stands.

Checks the Builder did not run:
 - integrity: canon mariadb byte-identical across the destructive restore round-trip
   (cksum 4271926745 166164480, 386 files, before and after).
 - mutation testing (are the +10 tests vacuous?): reverting canonical_ns() to `return recipe`
   reds 4 of them; removing both _assert_slot_not_foreign() call sites reds 2. Not vacuous.
   Suites reproduce exactly: 325 passed @b5f2b10, 315 passed @07fc6d4.
 - caller audit: every snapshot/restore/app_dir/snap_dir call site passes an explicit slot;
   no bare recipe survives.
 - blast radius (the risk this refactor most plausibly created): all 21 enrolled recipes still
   resolve to their EXISTING on-disk dirs; registry_path("bluesky-pds") is character-identical
   at parent and fix. The 3 without a registry have no dir at all and never did. No migration.
 - prune_stale invariant now structural: <recipe>/ never gains a canonical.json, so a
   de-enrolled provider can no longer rmtree the reconciler's last_good (scratch-root sim,
   fake ns only — prune_stale calls `docker volume rm`).

Enrollment retained (WARM_CANONICAL=True), no silent de-enrollment. Both false
"can never touch each other" comments removed.

B-redfix-5 (reconciler rollback restore() outside the upgrade try/except) accepted as
NON-BLOCKING: verified verbatim present at parent 07fc6d4, so it predates the enrollment.
F-redfix-4 made it reachable; that path is now closed. Correctly filed, not silently fixed.

All six recipes now hold a fresh Adversary PASS; F-redfix-1/2/3/4 CLOSED; no open blocking
finding. Builder may re-assert ## DONE.

Node clean: throwaway volume + scratch removed, real warm root untouched (keycloak/ = last_good
only, no canon-keycloak/ created), canon volumes intact, live keycloak /realms/master 200 throughout.
2026-07-09 00:19:39 +00:00
faef8a9685 claim(m2-redfix): M2 RE-CLAIMED — F-redfix-4 remedied at redfix-m2-harness@b5f2b10 (warm state keyed by stack namespace); clearing condition re-run green on cc-ci, unit suite 315->325; awaiting Adversary
Some checks failed
continuous-integration/drone/push Build is failing
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FS8p1esg57UAC69riNvuBX
2026-07-09 00:13:57 +00:00
397c303188 journal(redfix): wake #8 — VETO F-redfix-4 reproduced independently, root-caused, fixed at b5f2b10; decision recorded (warm state keyed by stack namespace); B-redfix-5 residual filed
Some checks failed
continuous-integration/drone/push Build is failing
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FS8p1esg57UAC69riNvuBX
2026-07-09 00:13:47 +00:00
cfea875b2b status(redfix): claim WITHDRAWN — standing VETO F-redfix-4 (keycloak warm-state slot collision); M2 reopened, do not merge redfix-m2-harness
Some checks failed
continuous-integration/drone/push Build is failing
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FS8p1esg57UAC69riNvuBX
2026-07-09 00:01:57 +00:00
78af2c995a note(redfix): post-verdict JOURNAL consult — warm-state dimension never considered (F-redfix-4 is a blind spot, not a deferral)
Some checks failed
continuous-integration/drone/push Build is failing
2026-07-08 23:58:32 +00:00
941d5e81c1 review(redfix): VETO — keycloak enrollment not collision-free in warm state (F-redfix-4)
Some checks failed
continuous-integration/drone/push Build is failing
Post-reboot re-confirmation #8. New break-it probe on the two HARNESS fixes (previously
only sha-reachability checked, never content, never second-order effects).

mumble 07fc6d4: CLEAN — attempts 12->36 @5.0s = claimed 180s; all assertions unchanged,
so a dead server still FAILs. Budget widened without weakening the test.

keycloak 61211db: correct at the domain/stack layer, but its own invariant is FALSE.
canonical_domain() separates the two deployments by domain, yet warm STATE is keyed by
recipe: warmsnap.snap_dir("keycloak") is ONE slot shared by the live-warm reconciler
(warm-keycloak, stateful=True) and the newly-enrolled data-warm canonical
(warm-canon-keycloak, seeded via promote_canonical -> seed_canonical, no WARM_DOMAINS guard).

Proved by execution on cc-ci in a scratch CCCI_WARM_ROOT against the real idle canon stack:
snapshot(canon) -> snapshot(other stack) destroys the canonical known-good, and
restore(canon) raises SnapshotError. Fails closed (no cross-stack data write), but:
 1. every stateful reconciler upgrade deterministically destroys the canonical snapshot;
 2. the reconciler's rollback restore() is OUTSIDE its try/except and its snapshot->restore
    window spans deploy+wait_healthy (health_timeout 900). A sweep promote landing there makes
    the rollback raise AFTER abra.undeploy(live) -> live keycloak left undeployed = outage of
    the shared OIDC provider lasuite-*/drone depend on. Exactly the hazard canon §2.B's
    de-enrollment exception existed to prevent.
 3. prune_stale()'s documented "keycloak untouched" invariant becomes false once seeded.

Undetectable by the M2 run: /var/lib/ci-warm/keycloak/ holds only last_good (no canonical.json,
no snapshot/) vs cryptpad which has both — the promote deployed but seed_canonical never ran,
registry-advance being deferred to the operator's merge. First seed happens post-merge, unexercised.

VETO scoped to keycloak. M1 classifications and the discourse/mattermost-lts/gitea/bluesky-pds/
mumble fixes are unaffected and remain PASS. Clears when the two deployments use disjoint
warm-state paths and each restore() returns its own stack's volumes.

Node left clean: real warm root untouched, volumes intact, live keycloak 200 throughout.
2026-07-08 23:58:01 +00:00
1e513e7fe1 journal(redfix): wake #7 — consumed Adversary re-confirmation #7 (content-anchored break-it probe: each M2 fix sha grepped in a cold bare clone with its parent as negative control; mattermost-lts/gitea/bluesky-pds fixes present, absent in parents). All 4 recipe fixes now content-anchored, not sha-anchored. DONE stands, no VETO, no inbox, no action. Loop stopped
Some checks failed
continuous-integration/drone/push Build is failing
2026-07-08 23:49:44 +00:00
4682aa1216 review(redfix): post-reboot re-confirmation #7 — DONE stands, no VETO. Zero delta since #6 (Builder JOURNAL append only). New break-it probe: verified fix CONTENT (not just sha reachability) at every M2 anchor via cold bare clones — mattermost-lts 4ca7f418 adds backupbot.restore.post-hook, gitea a0f2db8 adds app.ini.init + docker-setup cp, bluesky-pds 4987ba9 adds FQ APP_HOST with no leftover bare app alias — each with its parent commit as a negative control (0 matches pre-fix). All 4 recipe fixes now content-anchored. All findings closed
Some checks failed
continuous-integration/drone/push Build is failing
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHWWoceR5fHLTEwW8b3QnQ
2026-07-08 23:42:52 +00:00
7ac7eb078b journal(redfix): wake #6 — consumed Adversary re-confirmation #6 (break-it probe of M2 evidence anchors: mattermost-lts/gitea/bluesky-pds/harness-branch all reachable with matching subjects; rot confined to discourse, already CLOSED as F-redfix-3). DONE stands, no VETO, no inbox, no action. Loop stopped
Some checks failed
continuous-integration/drone/push Build is failing
2026-07-08 23:36:25 +00:00
c6593bbf6e review(redfix): post-reboot re-confirmation #6 — DONE stands, no VETO. Zero delta since #5 (only a Builder JOURNAL append). Break-it probe on evidence-anchor rot: cold bare-cloned all 3 remaining recipe mirrors — mattermost-lts 4ca7f418, gitea a0f2db8, bluesky-pds 4987ba9 all REACHABLE with subjects matching their asserted fixes, and harness branch redfix-m2-harness still pins exactly 07fc6d4 (mumble) with 61211db (keycloak) reachable. Rot confined to discourse (F-redfix-3, CLOSED). All findings closed
Some checks failed
continuous-integration/drone/push Build is failing
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WY7MNKwa6gascKiXER9ZbN
2026-07-08 23:36:00 +00:00
0302cb2589 journal(redfix): wake #5 — consumed Adversary re-confirmation #5 (audit of my F-redfix-3 addendum: clean, remedy verified, fix commit still reachable). DONE stands, no VETO, no action. Loop stopped
Some checks failed
continuous-integration/drone/push Build is failing
2026-07-08 23:30:05 +00:00
805c44e42d review(redfix): post-reboot re-confirmation #5 — DONE stands, no VETO. Audited Builder's F-redfix-3 addendum (612412c): append-only (+54/-0), my files + DONE line + historical shas untouched, no gate reopened. Ran its published repro verbatim in a fresh clone: ede6399 → official image 3.5.3 + 0 sidekiq, both EXPECTED met. Ruled out vacuous zero (file exists, blob, services:app: present) and added negative control (d7c8c47 added sidekiq, 0c4539b removed it) — fix COMMIT still reachable, only sha labels rotted. F-redfix-3 remedy verified, not just asserted
Some checks failed
continuous-integration/drone/push Build is failing
2026-07-08 23:29:09 +00:00
612412c52a status(redfix): evidence addendum for F-redfix-3 — durable content assertion for the discourse fix (sha pins rotted via later-phase force-push; content re-verified at ede6399); historical shas left unedited on purpose. DONE unchanged, no gate reopened
Some checks failed
continuous-integration/drone/push Build is failing
2026-07-08 23:23:33 +00:00
1271c248b9 review(redfix): post-reboot re-confirmation #4 — DONE stands, no VETO (M1+M2 intact, both findings closed) + NEW non-blocking F-redfix-3 (CLOSED): discourse M2 evidence shas 9ff5e19/53ba0910 gone from mirror (branch rebased+extended by later phases), but fix CONTENT re-verified at HEAD ede6399 — official image + 0 sidekiq; other 3 recipe shas + harness branch pin exactly
Some checks failed
continuous-integration/drone/push Build is failing
2026-07-08 23:22:05 +00:00
001a1f4d4d journal(redfix): post-reboot re-confirmation #4 — phase still DONE (M1+M2 PASS, no VETO, no inbox); F-redfix-2 gitignore remedy independently re-tested and holds on origin/main; stray untracked main.go surfaced, not deleted
Some checks failed
continuous-integration/drone/push Build is failing
2026-07-08 23:20:57 +00:00
bccba44b00 journal(redfix): F-redfix-2 CLOSED by Adversary — no action required, phase stays DONE
Some checks failed
continuous-integration/drone/push Build is failing
Read review(redfix) @67e86de: no VETO, M1/M2 PASS intact, remedy cold-verified
from a fresh clone of origin/main. Nothing claimed of me.

Records the Adversary's self-caught grep contamination (searching history for a
secret by a 6-char prefix that our own finding/inbox/journal text now contains
tests our prose, not history; re-test against the full 51-char value: 0 commits,
never leaked) and its acceptance of my symlink correction.

Residual unchanged: key on disk unrotated -- operator's call, not either loop's.
Did not edit the Adversary-owned findings section. Terminal condition holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018nVVSxRnj3K5MxGGRJzvTe
2026-07-08 23:14:36 +00:00
67e86ded88 review(redfix): F-redfix-2 CLOSED — gitignore remedy cold-verified (fresh-clone + add -A replay, origin/main, no HTTP exposure); full 51-char key never committed (my prefix-grep was self-contaminated); Builder's symlink correction confirmed — one repo, fix fully applied; rotation left to operator; DONE stands, no VETO; consumed ADVERSARY-INBOX
Some checks failed
continuous-integration/drone/push Build is failing
2026-07-08 23:13:26 +00:00
a10b2e8275 inbox(redfix): notify Adversary F-redfix-2 remedy pushed; correct "both clones" premise (symlink => one repo)
Some checks failed
continuous-integration/drone/push Build is failing
Journal: /srv/cc-ci/cc-ci and /srv/cc-ci-orch/cc-ci are the same directory
(.gitignore inode 3252849 shared; rev-parse --show-toplevel agrees), as are the
cc-ci-adv pair, which carries no config.json. So the single .gitignore line
closes the exposure on every path; no second remediation pending.

Escalated to operator: rotate the Tinfoil key if it ever hit a transcript/log --
gitignore prevents a future commit, it cannot un-expose an already-seen key.

Phase redfix remains DONE. F-redfix-2 stays open; only the Adversary closes it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018nVVSxRnj3K5MxGGRJzvTe
2026-07-08 23:09:42 +00:00
8cf08fd834 fix(redfix): gitignore config.json (F-redfix-2 remedy) + consume BUILDER-INBOX
Some checks failed
continuous-integration/drone/push Build is failing
Adversary finding F-redfix-2 (non-blocking): config.json sits untracked AND
un-gitignored at both Builder clone roots holding a live-shaped Tinfoil API key
(.provider.tinfoil.options.apiKey) -- one `git add -A` from being pushed to
git.autonomic.zone. Independently re-verified all four claims from my own clone:
untracked, never committed (git log --all -- config.json empty), not ignored,
key live-shaped. Latent risk, NOT an existing leak.

Remedy: add config.json to the "local secrets / env -- never commit" block.
Proved effective by replaying `git add -A` against a scratch GIT_INDEX_FILE:
config.json is no longer staged (only .gitignore, main.go).

Did not delete/move config.json (foreign file, not created by this loop), did
not rotate the key (operator's call -- escalated), did not gitignore main.go
(foreign, but no secret in it), did not reopen redfix or touch REVIEW-redfix.md.
Phase redfix stays DONE; only the Adversary closes F-redfix-2, after re-test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018nVVSxRnj3K5MxGGRJzvTe
2026-07-08 23:08:27 +00:00
a16c367091 review(redfix): DONE re-confirmed post-reboot #3 (M1+M2 PASS, no VETO) + NEW non-blocking finding F-redfix-2 — live API key in un-gitignored config.json at both Builder clone roots (never committed; one 'git add -A' from a push)
Some checks failed
continuous-integration/drone/push Build is failing
2026-07-08 23:06:44 +00:00
418ec570ef journal(redfix): post-reboot re-confirmation #2 — phase still DONE (M1+M2 PASS, no VETO); record warm-gitea pre-merge crash-loop (= M1 root cause, awaits PR #2), foreign wedged procs, and untracked root files left untouched (config.json holds a live-looking API key)
Some checks failed
continuous-integration/drone/push Build is failing
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKptqtbUkaLHDCmwomwDjy
2026-07-08 23:01:02 +00:00
ddf3788483 note(redfix): post-reboot re-confirmation — phase remains DONE (M1+M2 PASS, no VETO); node healthy; no pending claim; loop stopped
Some checks failed
continuous-integration/drone/push Build is failing
2026-07-08 20:29:07 +00:00
10 changed files with 4909 additions and 4 deletions

2
.gitignore vendored
View File

@ -3,6 +3,8 @@
*.key
*.pem
!docs/**/*.pem
# holds a live provider API key at .provider.tinfoil.options.apiKey (F-redfix-2)
config.json
# python
__pycache__/
*.pyc

2
.gitmodules vendored
View File

@ -1,3 +1,3 @@
[submodule "secrets"]
path = secrets
url = https://git.autonomic.zone/recipe-maintainers/cc-ci-secrets.git
url = git@git.autonomic.zone:recipe-maintainers/cc-ci-secrets.git

View File

@ -51,10 +51,352 @@ hold). Concrete fix designs from M1 evidence:
doing the migration (major rewrite — official discourse image is launcher-based, likely
infeasible cleanly). Lean (a)+tracked-upstream; may need operator input (DEFERRED?) — assess in M2.
### M3 — post-VETO remediation (F-redfix-4)
- [x] **keycloak warm-state slot collision** — FIXED at `redfix-m2-harness`@`b5f2b10`. `canonical_ns()` is
now the one namespace behind both the canonical's domain and its warm-state slot; live-warm provider
`canon-<recipe>` slot, disjoint from the reconciler's `<recipe>/`. Plus a naming-independent
`_assert_slot_not_foreign()` guard. Unit suite 315→325; clearing condition re-run green on cc-ci
(each `restore()` returns its own stack's volumes; reconciler `last_good` survives). Verify per
STATUS-redfix.md "Gate: M2 RE-CLAIMED".
- [ ] **B-redfix-5 — reconciler's post-`undeploy` warmsnap calls are outside the upgrade's `try/except`**
(NOT blocking; NOT part of F-redfix-4's clearing condition; recorded so it is not silently dropped).
In `warm_reconcile.py` there are **two** such sites, both after `abra.undeploy(domain)` and both
outside the `try/except` that guards the upgrade:
**(a) upgrade path** `:512-514``abra.undeploy``wait_undeployed``warmsnap.snapshot(...)`;
**(b) rollback path** `:534-536``abra.undeploy``wait_undeployed``warmsnap.restore(...)`
`deploy_version(last_good)` at `:537`.
If either raises for ANY reason (absent/corrupt snapshot, foreign slot, docker error, "no volumes
found") the exception propagates, the following `deploy_version` never runs, and live keycloak is left
**undeployed**. Site (a) is on the *normal* upgrade path — it does not need a rollback to fire.
**Reachability, corrected (wake #42):** F-redfix-4's fix does not remove the trigger, it *adds* one —
`_assert_slot_not_foreign()` is a new raise inside BOTH `snapshot()` (`warmsnap.py:158`) and
`restore()` (`:209`). It is unreachable on cc-ci **today** because the live slot holds no
`snapshot/meta.json` (`read_meta``None`, and an unclaimed slot is free to claim) — *not* because
F-redfix-4 is closed. A pre-fix (`07fc6d4`) canonical seed is the one state that writes a foreign
domain into the live slot; see the MERGE PRECONDITION in STATUS-redfix.md.
**Consequence, corrected (wake #44, A-redfix-4) — worse than recorded above, still not blocking.**
The wedge is neither manual-recovery-only (my `68b51d5`) nor reboot-healed (A-redfix-2). A weekly
`nightly-sweep.timer` (`OnCalendar=Sun *-*-* 03:00:00`, `Persistent=true`) re-invokes
`warm_reconcile.py keycloak` via `nightly_sweep.roll_warm_infra()` (`:60`), so once armed the wedge
**alternates DOWN/UP every 7 days, indefinitely**: sweep N wedges at site (a); sweep N+1 takes the
fresh-deploy branch (`:471-479`, no `warmsnap`) and comes back up on the old version; sweep N+2
wedges again. It is also **silent**`roll_warm_infra()` discards the subprocess rc (`:59-62`) so
`nightly-sweep.service` reports `Result=success`, and site (a) is upstream of every `write_alert()`
(`:493,500,503,539`). A weekly outage of the shared SSO provider would surface nowhere. Verified
first-hand against the node, not adopted from the finding.
**Not armed, and not self-arming:** `origin/main` — the tree the sweep executes (`CCCI_REPO=/etc/cc-ci`)
— has `WARM_CANONICAL = False`; only `07fc6d4` and `b5f2b10` have `True`, and `b5f2b10` ships the
`canonical_ns()` fix so its seeds land in `canon-keycloak/`. The binding rule is **never deploy
`07fc6d4`**.
Remedy sketch: wrap both sites so a warmsnap failure still redeploys `last_good` (or, if restoring
data is judged mandatory before redeploy, alert loudly + leave a breadcrumb rather than dying mid-
rollback), and make `roll_warm_infra()` propagate a non-zero rc so the failure is at least visible.
Needs a decision on which is safer for a DB-backed app after a forward migration — that
trade-off is why this is filed, not fixed inline. **Fixing it now would move the merge target off
`b5f2b10`, against which the M2 PASS was given, so it stays deferred for the operator.**
- [ ] **B-redfix-6 — `canonical_ns()` docstring says "the 15 existing canonicals"; the real number is 17,
and the invariant is not a count** (COSMETIC; docs-only; no behaviour change). At
`redfix-m2-harness`@`b5f2b10`, `runner/harness/canonical.py:52` reads "zero blast radius on the 15
existing canonicals". Verified on live disk: `/var/lib/ci-warm/` has 20 slots, **17** carrying a
`canonical.json` (spared: `alerts`, `keycloak`, `traefik` — the reconciler dirs). More importantly the
guarantee is *structural*, not numeric: `WARM_DOMAINS` is a singleton (`{"keycloak"}`), 21 recipes are
enrolled, so exactly one re-keys (`keycloak -> canon-keycloak`) and the other 20 satisfy
`canonical_ns(r) == r`. The docstring's count will rot again on the next enrollment.
**Deliberately NOT fixed inline:** amending it would move the branch tip off `b5f2b10`, the exact sha
the M2 PASS was granted against and that every drift sweep pins. Fold into the next commit that moves
the branch for a substantive reason (e.g. B-redfix-5), rewording to the structural form.
Corrected in STATUS-redfix.md prose (which is not sha-pinned) as of wake #17.
- [ ] **B-redfix-7 — orphaned non-Nix clone at `/etc/cc-ci` on the node stores the Gitea bot password in
plaintext in a world-readable `.git/config`** (OPERATOR CALL; out of redfix scope; found wake #18 while
checking a claim in re-confirmation #16). Facts, all read-only-verified on cc-ci:
- `/etc/cc-ci/.git/config` is mode **644** `root:root` and its `origin` URL embeds
`https://autonomic-bot:<password>@git.autonomic.zone/...` — the bot's Gitea credential at rest in
cleartext. (Value deliberately not reproduced here.)
- **Severity is LOW, not nil — but my first rationale for that was WRONG** (corrected wake #19 after
Adversary re-confirmation #17; filed Adversary-side as **A-redfix-1**). I originally argued "no
non-root *login* users, so only root can read it." That is the wrong axis: a process needs no login
shell to run as uid 1000, and mode 644 permits **any** uid to read. Verified directly —
`setpriv --reuid=1000 … /etc/cc-ci/.git/config` **succeeds** in the host namespace.
- **What actually contains the blast radius is mount-namespace isolation**, re-derived first-hand:
exactly ONE non-root process shares pid1's mount ns (`dbus-daemon`, uid 4). Every other non-root
process is containerized — notably the uid-1000 Quarkus `java` that IS the internet-facing
warm-keycloak: its `ns/mnt` is `4026533305` vs pid1's `4026531841`, and
`ls /proc/<pid>/root/etc/cc-ci`**No such file or directory**. Mode-permits ≠ reachable.
- **Therefore the containment is load-bearing on a fragile property** and dies silently if a non-root
host-namespace daemon ever appears, or if `/etc` is bind-mounted into any container. That fragility —
not the current reachability — is the reason to fix it.
- `/etc/cc-ci` is a **real directory, not a `/nix/store` symlink**, and **no systemd unit references
it** — i.e. undeclared, unmanaged server state that nothing runs from. It violates the standing
"keep server state Nix-declared and reversible" rule.
**NOT actioned by me, deliberately.** The Gitea bot credential is a Class-A1 EXTERNAL infra input
(plan §4.4) — not mine to rotate or invent. And I did not create `/etc/cc-ci`, so it is not mine to
delete. Remedies for the operator, cheapest first: (a) `chmod 600` the config; (b) move the credential
to a `credential.helper` / netrc outside the repo; (c) if the clone is genuinely orphaned, remove it and
let Nix own any needed checkout. Rotating the bot password is worth considering regardless, since it has
sat in cleartext on disk.
- [ ] **B-redfix-8 — the live Gitea bot password was committed to this repo, pushed to `origin/main`, and is
served to the UNAUTHENTICATED PUBLIC INTERNET** (**SEVERITY HIGH — URGENT operator rotation, not
deferrable**; found + redacted wake #19; public-exposure confirmed wake #20).
**Public exposure — independently verified wake #20 (not taken from the Adversary's report):** a plain
`urllib.request.urlopen` (no auth handler, no netrc, no git credential helper) of
`…/recipe-maintainers/cc-ci/raw/commit/14c7dee/machine-docs/BACKLOG-redfix.md` returns **HTTP 200, 33408
bytes, with the cleartext password in the body** (sanity-checked: body starts `# BACKLOG`, contains the
A-redfix-1 `Repro` block — a real fetch, not an error page). Cross-check that the fetch is intact:
`git cat-file -s 14c7dee:machine-docs/BACKLOG-redfix.md``33408`; a raw blob at a fixed commit sha is
immutable, so the served size MUST equal the object size. If your probe returns 33408, that is the leak,
not an error page. (Both figures were recorded as `33080` until 2026-07-09T02:24Z — a transcription slip
caught by the Adversary and re-measured against the blob; see REVIEW re-confirmation #22.) The mirror is public. So the leaked
credential is now **world-readable to anyone on the internet, with no account**, permanent in history,
replicated to every clone, AND push-capable to `recipe-maintainers/*`.
**STILL THE LIVE CREDENTIAL — measured, not assumed (wake #23, 2026-07-09).** Public *reachability*
(HTTP 200) and the value being *unrotated* are two different claims; until wake #23 only the first had
ever been measured, and "unrotated" was carried forward by repetition. Direct check: the current
`GITEA_PASSWORD` from `/srv/cc-ci/.testenv` is present verbatim in the `14c7dee` blob → **True**. So the
bytes the public internet serves ARE the password the harness authenticates with today; the exposure is
**not inert**. Operator re-check without printing the secret (verified to run, wake #23 — copy verbatim):
python3 -c 'import hashlib,re;v=re.search(r"^GITEA_PASSWORD=(.*)$",open("/srv/cc-ci/.testenv").read(),re.M).group(1).strip().strip(chr(34)).strip(chr(39));print(hashlib.sha256(v.encode()).hexdigest()[:16])'
→ prints `3fcea78925015fc9` **while still unrotated**. A different digest ⇒ rotation happened ⇒
`14c7dee` is inert ⇒ this item can be closed. (The digest commits to the leaked value without
republishing it; `sha256` of the raw password, first 16 hex chars.) HEAD (`main`) and the redaction
commit `e99e2b3` were re-fetched publicly and are **clean** — only the historical commit `14c7dee`
serves it. This lifts A-redfix-1/B-redfix-8 from LOW (host-local, mount-ns-contained) to **HIGH**:
mount-namespace isolation is irrelevant once the same secret is on the open web.
While documenting A-redfix-1, `machine-docs/BACKLOG-redfix.md` gained a "repro" line that inlined the
**actual bot password** as a `grep` pattern. Introduced by commit **`14c7dee`**, which is **on
`origin/main`** — i.e. pushed to `git.autonomic.zone/recipe-maintainers/cc-ci`.
**This strictly escalates A-redfix-1.** That finding was a secret in one 0644 file on one host,
reachable only from pid1's mount namespace. This moved the same secret into a **git repository**, which
is: replicated to every clone (both loops' clones and the node's `/etc/cc-ci`), readable by anyone with
read access to the mirror, and — the sharp edge — the credential grants **push** access to
`recipe-maintainers/*`, so it now sits inside a repo it can write to.
**Done:** redacted from `HEAD` (wake #19); the repro now greps `autonomic-bot:` instead, verified to
return the same `1`, so the finding lost no verifiability.
**NOT done, and cannot be by me:** the secret remains in **history at `14c7dee` forever**. Excising it
needs a history rewrite + `--force`, which the standing rules forbid ("Never `--force`"), and which
would break both loops' clones. Redaction at HEAD stops propagation; it does **not** undo disclosure.
**⇒ Rotate the `autonomic-bot` Gitea password — URGENT.** It is a live, world-readable, push-capable
credential; every hour it stays valid it can be used by anyone who has fetched that public URL. It is a
Class-A1 external input, so only the operator can rotate it. **Rotation is the ONLY remediation that
actually closes this** — a history rewrite is both forbidden here (`--force`) and insufficient anyway,
because a value already served publicly must be presumed captured/cached/crawled and cannot be
un-published. Once rotated, `14c7dee` becomes inert. The re-issued credential must NOT go back into a
remote URL — see A-redfix-1's remedy ladder (`credential.helper` / netrc outside the repo).
**Process lesson (mine):** my pre-commit guard did catch this, but I had chained it with `;` instead of
`&&`, so the commit proceeded anyway and I pushed on top of the leak. A guard whose failure does not
halt the pipeline is decoration. Fixed by making the check `&&`-gated before `git commit` in wake #19+.
- **B-redfix-9 — harden `CCCI_SKIP_FETCH` staging: it copies credentialed `.git/config` into a world-readable
run tree.** *(DEFERRED — post-merge; outside redfix DoD; no gate impact. Filed wake #54.)*
`fetch_recipe`'s staging branch (`runner/run_recipe_ci.py:348-353`) does
`shutil.copytree(~/.abra/recipes/<recipe>, <run-dir>)`. The canonical clones carry the bot password AND a
live oauth2 token in their remotes but are shielded by `/root` = `0700`; the run tree `/var/lib/cc-ci-runs`
is `0755`, so the copy **loses the protection of its source** — 133 world-readable cred-bearing copies today
(78 password, 117 token, overlapping). **CORRECTED (wake #55): this regenerates WEEKLY, not just on hand-runs.**
The `manual-*` prefix does not mean hand-run: the autonomous `nightly-sweep.timer` runs `run_recipe_ci.py`
outside Drone (`nightly_sweep.py:88` sets `CCCI_SKIP_FETCH="1"`), and `run_id()` labels every non-Drone run
`manual-<pid>` (`run_recipe_ci.py:318-319`). So the sweep itself is the generator and re-arms the exposure
each fire. (My earlier "0 production runs carry it, all `manual-*` = leftovers" mis-read `manual-*`; the
normal Drone fetch path IS clean — clean URL + per-command `http.extraHeader` token, landed `9b33fdf` — but
the sweep does not take that path.)
**Fix:** after `copytree`, strip userinfo from the copied remote (`git -C <dest> remote set-url origin <clean>`),
or exclude `.git/config` from the copy and re-init the remote clean; plus `chmod 0750 /var/lib/cc-ci-runs`.
**Do not** rely on rotating the password: rotation without this re-exposes the *new* value on the next staged run.
## Adversary findings
(Adversary-owned — do not edit.)
### A-redfix-1 [adversary] — `/etc/cc-ci/.git/config` is 0644 and embeds the Gitea bot password in cleartext (severity LOW, but for a different reason than B-redfix-7 states)
Independently confirmed the Builder's B-redfix-7. Out of redfix scope; **no VETO, does not reopen the phase**
(pre-existing undeclared server state, not created by redfix, not covered by any DoD item). Filed so the
severity *rationale* is right, because the wrong rationale would let the mitigation evaporate silently.
**Repro (read-only, from any shell with `ssh cc-ci`):**
stat -c '%a %U:%G %n' /etc/cc-ci/.git/config # -> 644 root:root
grep -c 'autonomic-bot:' /etc/cc-ci/.git/config # -> 1 (cleartext bot password in origin URL)
# [REDACTED by Builder, wake #19] the line above originally inlined the LIVE bot password as the
# grep pattern. Matching on the username is an equivalent repro and leaks nothing. See B-redfix-8.
stat -c '%a %n' / /etc /etc/cc-ci /etc/cc-ci/.git # -> 755 on every parent: world-traversable
**Why LOW — the correct reason.** B-redfix-7 argues "no non-root *login* users exist (no uid>=1000 with a
real shell)". That is the wrong axis: the risk is any non-root *code execution*, login shell or not. The
mode genuinely permits it —
setpriv --reuid=1000 --regid=1000 --clear-groups cat /etc/cc-ci/.git/config # -> prints the password
— so the file is readable by uid 1000. What actually holds the severity down is **mount-namespace
isolation**, which neither of us had checked: *every* non-root process on the box is containerized and its
`/etc` is not the host's. I enumerated it directly (compare `/proc/<pid>/ns/mnt` against `/proc/1/ns/mnt`):
of all non-root processes, exactly **one** lives in the host mount namespace — `dbus-daemon`, uid 4
(`messagebus`), not network-facing. Everything else (incl. the uid-1000 `java` = the warm-keycloak Quarkus
container) is in its own namespace and **cannot** reach host `/etc/cc-ci`; verified:
`ls /proc/<keycloak-pid>/root/etc/cc-ci/.git/config` -> *No such file or directory*.
**Correction to my own probe.** My first pass read the uid-1000 `java` process off `ps`, saw `setpriv` as
uid 1000 print the password, and was one step from concluding "the internet-facing Keycloak can steal the
Gitea bot credential." It cannot. `setpriv` ran in the **host** namespace; the real uid-1000 process does
not. Mode-permits != reachable-by-a-real-process. Recording the near-miss so it is not re-derived as fact.
**Why it still matters.** The mitigation is incidental, not designed. It fails the moment anyone (a) runs a
non-root daemon in the host namespace, or (b) bind-mounts `/etc` into any container. Contrast the 28 other
`.git/config` files carrying the same cleartext credential under `/root`: those are protected by design —
`/root` is `0700`, and the control probe confirms it
(`setpriv --reuid=1000 … cat /root/.abra/recipes/drone/.git/config` -> *Permission denied*).
`/etc/cc-ci` is the sole copy whose parents are world-traversable.
**ESCALATION (wake #20, Adversary) — the disclosure is PUBLIC and unauthenticated; severity is no longer LOW.**
My own commit `14c7dee` inlined the live password as a grep *value* and pushed it to `origin/main`. The
Builder redacted `HEAD` (`e99e2b3`) and filed B-redfix-8 — correct, and it stops forward propagation — but
the secret is permanent in **history** at `14c7dee`, and I have now established the fact neither of us had
checked: **`git.autonomic.zone/recipe-maintainers/cc-ci` is a public mirror.** An **unauthenticated** HTTP
GET (no creds, plain `urllib`, git's `insteadOf` cred-injection explicitly bypassed) of
`…/raw/commit/14c7dee/machine-docs/BACKLOG-redfix.md` returns **200 with the cleartext password in the body**.
So the credential is: in git history permanently, replicated to every clone, **served to anyone on the
internet**, and it grants **push** to `recipe-maintainers/*`. That is a live, world-readable, write-capable
credential — the worst quadrant, and strictly worse than the on-disk 0644 file A-redfix-1 originally
described. This reclassifies the finding from LOW to **HIGH/urgent**. Only rotation clears it; a
history-rewrite + force-push is forbidden by the standing rules and would not help once the value is already
public. My scratchpad copy of the config was purged.
**Operator action (agreed with Builder: not ours).** Rotate the `autonomic-bot` Gitea token (Class-A1
external input, plan §4.4), then `chmod 600 /etc/cc-ci/.git/config` — or delete `/etc/cc-ci`, which is an
orphaned real dir (not a `/nix/store` symlink) referenced by no systemd unit. `/root/.git-credentials` is
already `0600`. **Closable only by me, after re-test.**
### [adversary] F-redfix-4 — keycloak enrollment is collision-free in DOMAIN but NOT in warm-state: the live-warm reconciler and the new data-warm canonical share one per-recipe snapshot slot — **CLOSED @2026-07-09T00:18Z (VETO CLEARED)**
**CLOSED by Adversary re-test.** Fixed at `redfix-m2-harness`@`b5f2b10` (parent `07fc6d4`): `canonical_ns()`
is now the single namespace behind BOTH the canonical's domain and its warm-state slot, so a live-warm
provider gets slot `canon-<recipe>/`, disjoint from the reconciler's `<recipe>/`. Plus a
naming-independent `_assert_slot_not_foreign()` guard on snapshot AND restore.
My cold re-test (full evidence in REVIEW-redfix.md @2026-07-09T00:18Z): the published clearing condition is
met verbatim — slots disjoint, `restore(canon)` and `restore(live)` each return their OWN stack's volumes,
reconciler `last_good` survives, foreign snapshot *and* foreign restore both refused. Beyond the Builder's
own checks I added four: (a) the canon mariadb volume is byte-identical across the destructive restore
round-trip (`4271926745 166164480`, 386 files); (b) **mutation testing** — reverting `canonical_ns()` reds 4
of the new tests, removing the guard reds 2, so the 315→325 test delta is not vacuous; (c) every
`snapshot`/`restore`/`app_dir` caller now passes an explicit slot, no bare recipe survives; (d) all 21
enrolled recipes still resolve to their existing on-disk dirs (`registry_path("bluesky-pds")` is
character-identical at parent and fix) — zero blast radius, no migration needed.
Consequences 13 resolved. Consequence 4 (`prune_stale`) is now structural: `<recipe>/` never gains a
`canonical.json`, verified in a scratch root. Enrollment retained (`WARM_CANONICAL = True`) — no silent
de-enrollment. The two false "can never touch each other" comments are gone.
Residual **B-redfix-5** (reconciler rollback `restore()` outside the upgrade's `try/except`) is NOT part of
this finding's clearing condition and is **not** blocking: I confirmed it is verbatim present at parent
`07fc6d4`, so it predates the enrollment. F-redfix-4 made it *reachable*; that path is now closed.
<details><summary>Original report (as filed 2026-07-08T23:56Z)</summary>
### [adversary] F-redfix-4 — original text — **OPEN, BLOCKING (VETO)**
**Severity:** BLOCKS the phase's keycloak DoD item and must be fixed before the operator merges
`redfix-m2-harness`. Worst case is an outage of the live shared OIDC provider that `lasuite-*`/`drone`
depend on — the exact hazard the original canon §2.B de-enrollment exception existed to prevent,
resurrected in a different namespace. Fails closed (raises), so **no silent data corruption**.
**What the M2 fix does (and it does work, as far as it goes).** `canonical.canonical_domain()` now routes
any recipe in `warm.WARM_DOMAINS` to `warm-canon-<recipe>`, and `tests/keycloak/recipe_meta.py` flips
`WARM_CANONICAL = True`. The two stacks are genuinely distinct at the docker layer — verified on cc-ci:
```
canonical_domain(keycloak) = warm-canon-keycloak.ci.commoninternet.net
WARM_DOMAINS[keycloak] = warm-keycloak.ci.commoninternet.net
stack_volumes(CANON) = ['warm-canon-keycloak_..._mariadb', 'warm-canon-keycloak_..._providers']
stack_volumes(LIVE) = ['warm-keycloak_..._mariadb', 'warm-keycloak_..._providers']
```
**The defect.** Warm *state* is keyed by RECIPE, not by domain:
`warmsnap.snap_dir(recipe) = $CCCI_WARM_ROOT/<recipe>/snapshot` and
`canonical.registry_path(recipe) = $CCCI_WARM_ROOT/<recipe>/canonical.json`.
So both stacks now share **one** snapshot slot, `/var/lib/ci-warm/keycloak/snapshot/`, which already
holds the live reconciler's sibling `last_good`. Two producers, two consumers, one slot:
| | producer | consumer |
|---|---|---|
| live-warm | `warm_reconcile.py:512` `snapshot(recipe, warm-keycloak…)` (stateful=True, pre-upgrade) | `warm_reconcile.py:534` `restore(recipe, warm-keycloak…)` (health-gate rollback) |
| data-warm | `canonical.seed_canonical``warmsnap.snapshot(recipe, warm-canon-keycloak…)` (via `run_recipe_ci.py:1047` `promote_canonical`, **no `WARM_DOMAINS` guard**) | `run_recipe_ci.py:896` `restore(recipe, warm-canon-keycloak…)` (quick-FAIL canonical rollback) |
`warmsnap.snapshot()` atomically **replaces** the slot; `warmsnap.restore()` reads `meta.json` by recipe
and then requires every recorded volume to exist in the *target* stack. Cross-stack names never match,
so restore raises `SnapshotError` instead of cross-writing data.
Consequences, in descending certainty:
1. **Deterministic — canonical known-good destroyed.** Every stateful reconciler upgrade of live keycloak
overwrites the canonical's snapshot. The canonical's WC4 quick-FAIL rollback (`run_recipe_ci.py:896`)
then raises `SnapshotError` and cannot roll back.
2. **Deterministic — reverse direction.** After a promote seeds the canonical, the slot holds canon volumes.
3. **Race, high impact — live SSO outage.** The reconciler's window between its pre-upgrade `snapshot()`
and its rollback `restore()` spans `deploy latest` + `wait_healthy` (`health_timeout: 900`). A nightly-sweep
`promote_canonical(keycloak)` landing in that window replaces the slot with canon volumes. The rollback
then does `abra.undeploy(live)``wait_undeployed``warmsnap.restore(...)`**raises**. `restore` sits
OUTSIDE the `try/except` that guards the upgrade, so the exception propagates and `deploy_version(last_good)`
never runs — **live keycloak is left undeployed**, taking SSO down for `lasuite-*`/`drone`.
4. **Latent — `prune_stale()` invariant now false.** Its docstring promises it "Leaves the live-warm reconciler
dirs (keycloak/traefik — they have a `last_good`, no `canonical.json`) untouched." Once keycloak is seeded it
*has* a `canonical.json`; if `WARM_CANONICAL` is ever flipped back to False, `prune_stale` matches it and
`shutil.rmtree(app_dir("keycloak"))` deletes the live reconciler's `last_good`.
**Why M2's verification could not have caught this.** The enrollment's data path never executed: on cc-ci,
`/var/lib/ci-warm/keycloak/` contains **only** `last_good` — no `canonical.json`, no `snapshot/` — while a normal
canonical (`/var/lib/ci-warm/cryptpad/`) has both. The `warm-canon-keycloak_*` volumes exist, so the promote
*deployed*, but `seed_canonical` never ran (registry-advance is deliberately deferred to the operator's merge).
The first-ever keycloak seed will therefore happen post-merge, in production, unexercised.
**The shipped code asserts the opposite.** `canonical.py` docstring: "a separate stack/domain that can never
touch the live provider"; `recipe_meta.py`: "separate deployments that can never touch each other… structurally
impossible." Both are false for warm state. That claim is what I falsified.
**Repro (cold, non-destructive — writes only to a scratch `CCCI_WARM_ROOT`; never touches the live stack).**
Uses the real idle `warm-canon-keycloak` stack and idle `warm-custom-html` as a stand-in for the live stack
(the live one cannot be undeployed to snapshot it):
```sh
ssh cc-ci
git clone -q --branch redfix-m2-harness <cc-ci remote> /tmp/p8 && cd /tmp/p8/runner
CCCI_WARM_ROOT=/tmp/p8w /nix/store/jag2131a95gw6ng7grig9pj3dn2q8vrv-python3-3.12.8-env/bin/python3 - <<'PY'
import sys; sys.path.insert(0, ".")
from harness import warmsnap as ws, canonical as c
CANON, STANDIN = c.canonical_domain("keycloak"), "warm-custom-html.ci.commoninternet.net"
print(ws.snap_dir("keycloak")) # one slot, domain-blind
ws.snapshot("keycloak", CANON, version="canon-known-good")
ws.snapshot("keycloak", STANDIN, version="live-last-good") # clobbers it
print(ws.read_meta("keycloak")["domain"]) # -> warm-custom-html… (canon known-good GONE)
ws.restore("keycloak", CANON) # -> SnapshotError
PY
```
EXPECTED (observed @2026-07-08T23:55Z):
```
/tmp/p8w/keycloak/snapshot <- SAME slot for BOTH domains
read_meta(keycloak).domain = warm-custom-html.ci.commoninternet.net <- canon known-good DESTROYED
SnapshotError -> snapshot volume warm-custom-html_ci_commoninternet_net_content
absent from current stack ['warm-canon-keycloak_..._mariadb', 'warm-canon-keycloak_..._providers']
```
Post-probe the node was verified untouched: `/var/lib/ci-warm/keycloak/` still `last_good` only, all three
volumes intact (159M / 8.0K / 40K, file counts unchanged), live `warm-keycloak…/realms/master` → 200. Scratch removed.
**Proposed remedy (Builder's to choose — mine to file, not to make).** Key warm state by the *stack/domain*
rather than the bare recipe for recipes in `WARM_DOMAINS` — e.g. `app_dir()` takes the domain, or the canonical
seeds under `<recipe>-canon/`. Then: fix `prune_stale`'s now-false invariant, and correct the two "can never
touch each other" comments. A guard alone (skip `seed_canonical` for `WARM_DOMAINS` recipes) would silently
de-enroll keycloak and re-open the DoD item, so it is not sufficient.
**Clears the VETO when:** the two stacks provably use disjoint warm-state paths, and a seeded keycloak canonical
survives a live-reconciler stateful upgrade (and vice versa) — demonstrated by re-running the repro above and
seeing each `restore()` return its OWN stack's volumes.
</details>
---
### [adversary] F-redfix-1 — discourse migration INCOMPLETE: dangling image-less `sidekiq` in compose.smtpauth.yml (R011 lint regression + breaks SMTP-auth deploys) — **CLOSED @2026-06-18T07:06Z**
**CLOSED by Adversary re-test.** Builder fixed in PR #4 @9ff5e19 (force-pushed onto 53ba0910): removed the
@ -107,3 +449,461 @@ ABRA_DIR=$LA script -qec "abra recipe lint -n discourse" /dev/null # -> R011 X
**Proposed remedy (recipe PR #4):** remove the orphaned `sidekiq:` block from `compose.smtpauth.yml` (fold
its `DISCOURSE_SMTP_PASSWORD_FILE` env + `smtp_password` secret into the `app` service, since sidekiq is now
internal). Re-run discourse cold -> EXPECT R011 OK, level=5. Only the Adversary closes this, after re-test.
### [adversary] F-redfix-2 — live API key sat untracked **and un-gitignored** at the Builder clone's repo root (`config.json`) — one `git add -A` from being pushed to origin — **CLOSED @2026-07-08T23:26Z**
**CLOSED by Adversary cold re-test.** Builder remedied @`8cf08fd`: `config.json` added to the "local secrets /
env — never commit" block in `.gitignore` (line 7, with a comment naming the finding). My independent
verification, none of it taking the Builder's word:
1. **Attack replay from cold**`git add -A` into a scratch `GIT_INDEX_FILE` seeded from HEAD: staged paths
are `main.go` only; `config.json` **not staged**. `git check-ignore -v config.json``.gitignore:7`.
2. **Fix is on origin, not just local**`git show origin/main:.gitignore` contains `config.json`. A *fresh
clone from origin* + dropping the real `config.json` in → ignored ✅, `git add -A` does not stage it ✅.
This matters: a local-only .gitignore edit would not protect the next clone.
3. **Full key never committed** — my original evidence used the 6-char prefix and is now **contaminated**: our
own finding/inbox/journal text contains `tk_bhg`, so `-S'tk_bhg'` yields false positives. Re-tested against
the *full 51-char value*: `git log --all -S"$KEY"`**0 commits** in BOTH `cc-ci` and `cc-ci-adv`. Binary
search on prefix length: the longest prefix ever committed anywhere is **6 of 51 chars**, in our own
documentation — not a usable disclosure. No leak, past or latent.
4. **No non-git exposure** — dashboard is live (`https://ci.commoninternet.net/` → 200) but
`/config.json`**404** (also 404 on `dashboard.ci.…`); no tracked source reads it (the other
`config.json` hits are `/root/.docker/config.json`, unrelated). Perms `-rw-r--r-- loops:users`.
**CORRECTION to my own finding (Builder was right, I was wrong).** I wrote "BOTH Builder clones". There is
only **one** repo: `/srv/cc-ci` is a symlink → `/srv/cc-ci-orch` (`ls -ld`), so `/srv/cc-ci/cc-ci` and
`/srv/cc-ci-orch/cc-ci` share `rev-parse --show-toplevel`, the same `.git` inode (3206558) and the same
`.gitignore` inode (3252849). My `cc-ci-adv` "pair" is the same illusion. A filesystem-wide sweep found
exactly one `config.json` inside any git repo, and it is now IGNORED. One fix, fully applied — not half.
**Residual, explicitly NOT closed by this:** the key is still on disk **unrotated** (`len=51`, `tk_bhg…`).
Gitignoring prevents a future commit; it cannot un-expose a value that leaked by another channel. Since the
full key provably never entered git and is not HTTP-reachable, git is not a reason to rotate. The Builder
correctly **escalated rotation to the operator rather than deciding it** — that judgement was right, and the
call remains the operator's.
---
<details><summary>Original report (as filed 2026-07-08T23:12Z)</summary>
### [adversary] F-redfix-2 — original text — **OPEN, NON-BLOCKING**
**Severity:** does NOT block phase `redfix` (out of scope of its Definition of Done — no VETO, DONE stands).
Latent secret-leak risk in the working environment; worth fixing before any future phase does a broad `git add`.
**What:** `config.json` (1128 B, mtime 2026-06-23T00:50Z) exists at the repo root of BOTH Builder clones —
`/srv/cc-ci/cc-ci` and `/srv/cc-ci-orch/cc-ci`. It holds a live-shaped inference credential at
`.provider.tinfoil.options.apiKey` (51 chars, prefix `tk_bhg…` — value not reproduced here). The file is
**untracked**, but `.gitignore` does **not** cover it: `.gitignore` lists `.testenv`, `*.key`, `*.pem`,
`runs/`, `.claude/` — no `config.json`. So `git check-ignore config.json` → miss.
Origin is a real pushed remote (`git.autonomic.zone/recipe-maintainers/cc-ci.git`, credentials embedded in
the remote URL). A single `git add -A` / `git add .` in either clone would stage and then push the key.
**Good news (verified, not assumed):** the key has never been committed —
`git log --all --oneline -S'tk_bhg'` → empty; `git log --all -- config.json` → empty; `git ls-files` has no
`config.json` at any path. So this is a *latent* risk, not an existing leak. The Adversary clones
(`/srv/cc-ci/cc-ci-adv`, `/srv/cc-ci-orch/cc-ci-adv`) do not carry the file at all.
**Repro:**
```
cd /srv/cc-ci/cc-ci && git status --porcelain config.json # -> "?? config.json"
git check-ignore -v config.json; echo "exit=$?" # -> exit=1 (NOT ignored)
git log --all --oneline -- config.json # -> empty (never committed)
```
**Proposed remedy (Builder — repo change, mine to file, not to make):** add `config.json` to `.gitignore`
under the existing "local secrets / env — never commit" block. Optionally rotate the Tinfoil key if it was
ever pasted into a log/transcript. I did **not** touch, move, or delete the file — it holds a live-looking
credential and is not mine to modify.
**Discovery:** independent break-it probe on my "no secrets in the repo / published logs / dashboard"
standing mandate, run after the phase closed. The Builder's journal @418ec57 independently noticed the same
file; I verified the exposure surface (gitignore miss + never-committed) from a cold start rather than
taking that note at face value. Only the Adversary closes this, after re-test.
</details>
### [adversary] F-redfix-3 — M2's discourse evidence shas (`9ff5e19`, `53ba0910`) no longer exist on the mirror; the fix content survives — **CLOSED @2026-07-08T23:24Z (non-blocking, no VETO)**
**Severity:** does NOT block phase `redfix` (DONE stands). Evidence-durability defect in the *record*, not in
the fix. Filed so a future auditor of redfix does not conclude "the discourse fix was withdrawn."
**What.** `STATUS-redfix.md` pins the discourse fix at `9ff5e19` (fix list) and `53ba0910` (WHERE refs) on
`recipe-maintainers/discourse` branch `discourse-official-image`. As of 2026-07-08 that branch heads at
`ede6399` and **neither sha resolves**: fetching all 17 `refs/heads/*` + `refs/pull/*/head` into one clone and
running `git cat-file -t` on each returns *not a valid object name*. The branch was force-pushed/rebased and
extended by **later** phases (`ede6399` = `refs/pull/5/head`; adds `discourse/postgres:pg18` + `POSTGRES_USER`
in `pg_backup.sh`). redfix's PR is also no longer "#4" — `refs/pull/4/head` is now `0c4539b7`.
**Why it is CLOSED rather than a VETO.** I re-verified the *content* the M2 PASS actually asserted, at the
current head: `compose.yml``image: discourse/discourse:3.5.3` (official-image migration) and
`compose.smtpauth.yml` → 0 `sidekiq` occurrences (the F-redfix-1 remedy). Both hold at `ede6399` and at
`0c4539b7`. The fix is present and re-verifiable; only the pointers rotted. M2 was correct when given.
**Repro.** `git clone https://git.autonomic.zone/recipe-maintainers/discourse && cd discourse && git fetch
origin 'refs/heads/*:refs/remotes/origin/*' 'refs/pull/*/head:refs/remotes/pr/*' && git cat-file -t 9ff5e19`
→ fatal. Then `git show origin/discourse-official-image:compose.yml | grep image:` → official image present.
(Note: `git fetch origin <sha>` and a `--filter=blob:none` clone both give false "absent" signals — use
reachability from all refs.)
**Lesson for future phases (no action required of the Builder now):** shared recipe branches get rewritten, so
a sha alone is not durable evidence. Record the *content assertion* (file → expected line) alongside the sha,
or push a tag. The other three redfix fixes pinned exactly (`4ca7f418`, `a0f2db88`, `4987ba91`), as did cc-ci
`redfix-m2-harness`@`07fc6d4a` — discourse drifted only because a later phase reused its branch.
### A-redfix-2 [adversary] — **CLOSED 2026-07-09T08:59Z** (accepted at `f64d102`; false half superseded by A-redfix-4 at `e356698`) — STATUS's "no code path redeploys it / recovery is manual" is REFUTED: the B-redfix-5 wedge **does** self-heal on the next unit activation, which makes it look intermittent
> **⚠ PARTIALLY WITHDRAWN by me at wake #44 (2026-07-09T08:5xZ) — see A-redfix-4.**
> The *first* half (a code path redeploys it → the "recovery is manual" wording is wrong) **stands**.
> The *second* half — "what blocks self-healing is the **absence of a re-trigger**; `warm-keycloak.service`
> has no timer / no `Restart=`; it heals on the next **reboot / `nixos-rebuild switch`**" — is **WRONG**.
> There IS an autonomous re-trigger: `nightly-sweep.timer` (`OnCalendar=Sun *-*-* 03:00:00`, `Persistent=true`)
> → `nightly_sweep.roll_warm_infra()` → `subprocess warm_reconcile.py keycloak` (`WARM_APPS=["keycloak",…]`).
> My probe was scoped to the wrong name: `list-timers | grep -i keycloak` is empty because the timer is called
> `nightly-sweep`, and `git grep warm-keycloak -- nix/` never sees the sweep. **Do not use either command.**
> Corrected dynamics, evidence, and remediation are in **A-redfix-4**, which supersedes this half.
Severity **LOW-MED**, **no VETO, does not reopen the phase** (operator-facing description of a *deferred*
item, not a DoD item). Merge target `b5f2b10` unchanged. Filed because the stated mechanism is wrong and the
wrong mechanism points the operator at the wrong remediation.
**What STATUS-redfix.md (`68b51d5`, lines ~91-94) claims:**
> "**The wedge does not self-heal.** … A raise at `:514` or `:536` therefore escapes to `SystemExit` with
> live keycloak already undeployed by the preceding `abra.undeploy()` — **no code path redeploys it.
> Recovery is manual.**"
**The conclusion "does not self-heal" is right; the reason given is wrong.** There *is* a code path that
redeploys it, and it runs automatically.
**Repro (static, from any clone; no node access needed):**
git show b5f2b10:runner/warm_reconcile.py | sed -n '468,479p' # the fresh-deploy branch
git show b5f2b10:runner/harness/abra.py | sed -n '295,298p' # undeploy does NOT remove the .env
sed -n '38,45p' nix/modules/warm-keycloak.nix # oneshot, RemainAfterExit, no Restart=
git grep -n "warm-keycloak" -- nix/ | grep -iE "timer|OnCalendar|Restart=" # -> empty
**Chain, each leg verified:**
1. `abra.undeploy()` runs only `abra app undeploy` — it does **not** remove `~/.abra/servers/default/<domain>.env`.
2. So on the next `reconcile("keycloak")`: `current_version(domain)` still resolves from `TYPE=` in that
`.env`, while `is_deployed(domain)` is `False` (no docker services for the stack).
3. `warm_reconcile.py:471-479` therefore takes the **fresh-deploy branch**: `target = current or latest`
`deploy_version(...)``wait_healthy``write_last_good``return deployed-fresh:<target>`.
**This branch never calls `warmsnap`**, so `_assert_slot_not_foreign` is never reached and the foreign
meta cannot block it. Live keycloak comes back up.
**What actually prevents self-healing** is not the absence of a redeploy path but the absence of a
**re-trigger**: `warm-keycloak.service` is `Type=oneshot`, `RemainAfterExit=true`, `wantedBy=multi-user.target`,
with **no timer and no `Restart=`** (the module comment says it "converges every activation/boot"). Nothing
re-runs it between activations.
**Why this matters operationally — the failure is intermittent, not sticky.**
- The wedge persists only until the next **reboot or `nixos-rebuild switch`**, at which point the unit
re-activates, hits the fresh-deploy branch and silently restores live keycloak. "Recovery is manual" will
mislead an operator into hand-redeploying something that a routine rebuild already fixed.
- But the foreign `snapshot/meta.json` is **still in the slot**. So the box heals, then **re-wedges the next
time an upgrade is actually due** (`current != latest` → upgrade path → `:514` `snapshot()` → guard raises
`abra.undeploy()` at `:512` has already run). Heal → re-wedge → heal: it will read as a *flake*.
- Therefore the correct remediation is **remove the foreign `snapshot/` from `/var/lib/ci-warm/keycloak/`**,
not "redeploy keycloak". Redeploying treats the symptom and the wedge returns on the next due upgrade.
**Ask of the Builder:** correct the two sentences in STATUS. Suggested: *"The wedge does not self-heal between
activations — `warm-keycloak.service` is a `oneshot` with no timer and no `Restart=`. The next reboot or
`nixos-rebuild switch` re-activates it and the fresh-deploy branch (`:471-479`, which never calls `warmsnap`)
does restore live keycloak — but the foreign meta remains, so it re-wedges on the next due upgrade. Remediation
is to remove the foreign `snapshot/`, not to redeploy."* The **precondition itself is unchanged and still
correct**; only the mechanism/remediation sentences are wrong.
### A-redfix-3 [adversary] — **CLOSED 2026-07-09T08:59Z** (accepted + evidence retracted at `f64d102`; STATUS:28-29,564-572 re-read and correct) — STATUS's `main.go` "present in both clones ⇒ written outside git" evidence is void: `/srv/cc-ci` is a **symlink** to `/srv/cc-ci-orch`, so there is one file, not two
Severity **INFO**. No VETO, no gate impact, no DoD impact. The *conclusion* (untracked, not from this phase,
leave it alone) is fine; the supporting evidence is an artifact.
**What STATUS-redfix.md (`42bc4e4`, lines ~470-472) claims:**
> "`/srv/cc-ci-orch/cc-ci/main.go` **and** `/srv/cc-ci/cc-ci/main.go` — present in **both** clones, identical
> size, identical mtime … i.e. written by something outside git (**a git operation cannot leave the same
> untracked file in two clones**)."
**Repro:**
ls -la /srv/ # cc-ci -> /srv/cc-ci-orch (symlink)
stat -c '%d:%i %n' /srv/cc-ci-orch/cc-ci/main.go /srv/cc-ci/cc-ci/main.go
# -> 2049:3254604 for BOTH; stat -c 'links=%h' -> links=1
Same device, same inode, link count 1: **one file, one clone, seen through a symlink.** The same holds for the
Adversary clones (`/srv/cc-ci-orch/cc-ci-adv` and `/srv/cc-ci/cc-ci-adv` are inode `3206945`). The "two clones"
observation carries **zero** information about the writer, and the identical size/mtime are tautological.
**Risk bounded (read-only):** contents are a 281-byte hello-world `net/http` listener; `sha256
bdbc3bf167cd20f30c00880005f4f994f17f3722660973c9c65c7bf33e81ffaf`; no `go.mod`; **`go` is not installed**;
**nothing is listening on `:8080`**; `git log --all -- main.go` is empty; not gitignored; unreferenced by any
tracked file. It cannot execute. Present in the **Builder's** clone only — **absent from both Adversary
clone paths**, which is consistent with a single stray write into one working tree.
**Ask of the Builder:** drop the "both clones ⇒ outside git" inference (keep the file, keep the flag). Agreed
on not deleting it: neither of us created it.
---
### A-redfix-4 [adversary] — **CLOSED 2026-07-09T08:59Z** — all three asks applied at `e356698` and cold-re-verified by me at wake #45 (probes deleted; oscillation + silence recorded; precondition re-anchored to "never deploy `07fc6d4`"). The underlying *behaviour* remains open under **B-redfix-5**, which is deferred, not fixed. — the B-redfix-5 wedge has a **weekly autonomous re-trigger** (`nightly-sweep.timer`) and fails **silently**: post-arming, keycloak oscillates down-a-week / up-a-week with `nightly-sweep.service` reporting `Result=success`
Severity **MED** (operator-facing; describes a *deferred* item's blast profile, **not** a DoD item).
**No VETO. Does not reopen the phase.** M1 + M2 PASS stand. Merge target `b5f2b10` unchanged and still correct.
Supersedes the "absence of a re-trigger" half of **A-redfix-2** (my own error) and, with it, the text the
Builder accepted into STATUS-redfix.md at `f64d102`.
**Both prior descriptions of the wedge are wrong, in opposite directions.**
- Builder (`68b51d5`): "no code path redeploys it. **Recovery is manual.**" → wrong; it recovers unattended.
- Me (`b358b7e`, A-redfix-2): "no re-trigger … heals on the next **reboot / `nixos-rebuild switch`**" → wrong;
it re-triggers **weekly**, and an unrelated `switch` does *not* heal it.
**The re-trigger.** `warm_reconcile.py` has **two** invokers, not one. The second is the sweep:
nightly-sweep.timer OnCalendar=Sun *-*-* 03:00:00, Persistent=true
└─ nightly-sweep.service → /etc/cc-ci/runner/nightly_sweep.py (CCCI_REPO=/etc/cc-ci)
└─ main():147 roll_warm_infra() # unconditional after the _another_run_active() guard
└─ for app in WARM_APPS = ["keycloak", "traefik"]: # nightly_sweep.py:39,57
subprocess.run([python, warm_reconcile.py, app]) # nightly_sweep.py:59-61
**Observational proof that the sweep — not `warm-keycloak.service` — reconciles keycloak** (cc-ci, cold):
systemctl show warm-keycloak.service -p ExecMainStartTimestamp # → Wed 2026-06-17 17:29:31 UTC (once)
stat -c %y /var/lib/ci-warm/keycloak/last_good # → 2026-07-05 03:04:51.209 +0000
systemctl list-timers --all | grep nightly-sweep # → last trigger Sun 2026-07-05 03:04:50
`write_last_good()` is called **only** from `reconcile()`. The slot file was written **18 days after** the unit
last ran, and **1.2 s after** the sweep fired. ⇒ `reconcile("keycloak")` ran from the sweep. (`journalctl -u
nightly-sweep.service` retains 1 line — the empty grep is *retention*, not absence. Do not read it as evidence.)
**Why both of our probes missed it — the tell.** The invoking timer's name never contains the app name:
systemctl list-timers --all | grep -i keycloak # → EMPTY, yet a timer DOES drive it
git grep -n warm-keycloak -- nix/ | grep -iE 'timer|Restart=' # → EMPTY, yet a timer DOES drive it
Both are **unfalsifiable by construction**. STATUS-redfix.md (`f64d102`) currently prescribes *both* as the
verification commands for "no re-trigger". They must be removed. The sound probe is to grep for the **callee**:
git grep -n "warm_reconcile" -- runner/ nix/ | grep -v "^runner/warm_reconcile.py"
Same class as the `e3b0c44298fc1c14` empty-input tell (B-redfix-8) and the `restore()`/`docker` ordering tell:
a probe that returns the "all clear" value for a reason unrelated to the property under test.
**Corrected dynamics** (only once armed — i.e. only if a foreign `snapshot/meta.json` sits in the slot **and**
an upgrade is pending, `current != latest`):
| sweep | state on entry | branch taken | outcome |
|---|---|---|---|
| N | deployed, upgrade pending | stateful upgrade `:511-514``abra.undeploy``snapshot()` → guard raises | **keycloak DOWN** |
| N+1 | not deployed | fresh-deploy `:471-479` → redeploys `current`; **never calls `warmsnap`** | keycloak UP (old version) |
| N+2 | deployed, upgrade still pending | stateful upgrade again → guard raises | **keycloak DOWN** |
**keycloak alternates DOWN one week / UP the next**, indefinitely, until the foreign `snapshot/` is deleted
or `current == latest`. Not a one-shot outage, and not manual-recovery-required.
**And it is silent.** `roll_warm_infra()` captures the subprocess rc and **ignores it** (`nightly_sweep.py:59-63`
`print(f"nightly: reconcile {app} rc={rc}")`, no raise), so a wedged reconcile does **not** fail
`nightly-sweep.service` (`Result=success`). The guard raises at `:514`, which is **outside** the try/except at
`:520-525` (that is B-redfix-5) and **upstream of every `write_alert()`** — so **no alert is written**. The only
trace is a journal line `nightly: reconcile keycloak rc=1`, in a unit whose output is not retained.
**A weekly production outage of the shared SSO provider would surface nowhere.**
**Good news — the trap cannot arm itself.** Arming requires the canonical seed to run from a tree that has the
keycloak enrollment but **not** the fix. The enrollment is **branch-only**:
git show origin/main:tests/keycloak/recipe_meta.py | grep WARM_CANONICAL # → False (= deployed /etc/cc-ci)
git show 07fc6d4:tests/keycloak/recipe_meta.py | grep WARM_CANONICAL # → True (enrollment, NO fix)
git show b5f2b10:tests/keycloak/recipe_meta.py | grep WARM_CANONICAL # → True (enrollment + fix)
The deployed checkout `/etc/cc-ci` carries `WARM_CANONICAL = False`, so **no autonomous node activity — including
Sunday's sweep — can write foreign meta into `/var/lib/ci-warm/keycloak/`.** Verified: the slot holds only
`last_good` (no `snapshot/`, no `canon-*`), and keycloak is currently deployed and healthy (`_app` + `_db`).
**⇒ The precondition is self-maintaining, and its landmark in STATUS is wrong.** It is not "the slot must be clean
*at merge time*" (a `git merge` executes nothing). The binding statement is:
> **Never deploy `07fc6d4`** — or any tree carrying the enrollment without `canonical_ns()`. That intermediate
> state is the *only* thing that can arm the trap. Merging `b5f2b10` ships enrollment **and** fix together, so
> canonical seeds land in `canon-keycloak/` and the slot never goes foreign.
**Secondary correction (`nixos-rebuild switch`).** `warm-keycloak.service`'s `ExecStart` embeds the runner by
store path — `/nix/store/…-runner/warm_reconcile.py`, and `warmsnap.py` lives in that **same** derivation
(`…-runner/harness/warmsnap.py`). So a switch restarts the unit **iff `runner/**` changed**. A switch that
touches anything else (e.g. sshd config) leaves the store path identical and does **not** re-run reconcile —
so "heals on `nixos-rebuild switch`" is false in general. Deploying the merge *does* change `runner/**`, which
means **the merge's own activation switch is itself a reconcile trigger**, alongside the Sunday sweep.
**Asks of the Builder (STATUS-redfix.md text only — no code change, nothing reopens):**
1. Delete both `grep -i keycloak` / `git grep warm-keycloak -- nix/` "no re-trigger" probes; they cannot fail.
2. Replace "recovery is manual" **and** "heals on reboot / `nixos-rebuild switch`" with the weekly
down/up oscillation above, and state that it is **silent** (`Result=success`, no `write_alert`).
3. Re-anchor the merge precondition from "slot clean at merge time" to "**never deploy `07fc6d4`**".
Remediation if ever armed is unchanged and still correct: **delete the foreign `snapshot/` from the slot**;
do not redeploy keycloak (the sweep does that for you, and it is what masks the fault).
---
### A-redfix-1 — ADDENDUM (wake #45, 2026-07-09T08:59Z): the embedded value is the **same live credential as B-redfix-8**, confirmed at value level; and rotation will **not** clean this file
Still **OPEN**. Re-verified cold on cc-ci this wake, without printing the secret:
ssh cc-ci 'stat -c "%a %U:%G" /etc/cc-ci/.git/config' # → 644 root:root
ssh cc-ci 'grep -oE "https://[^/@]+:[^/@]+@" /etc/cc-ci/.git/config \
| sed -E "s#https://[^:]+:##; s#@$##" | tr -d "\n" | sha256sum | cut -c1-16'
**EXPECTED / observed: `3fcea78925015fc9`** — byte-identical to the B-redfix-8 rotation sentinel
(`sha256(GITEA_PASSWORD)[:16]`, re-confirmed this wake from `/srv/cc-ci/.testenv` on the **orchestrator**).
So A-redfix-1 and B-redfix-8 are **two exposures of one still-unrotated, push-capable credential**, and the
sentinel matching proves it is **still not rotated**. (Extract-then-hash, never echo. `python3` is absent on
cc-ci — do not run the orchestrator's python probe over `ssh`; that is the `e3b0c44298fc1c14` empty-input tell.)
**New, and operator-relevant: rotation does not fix this file, and may recreate the exposure.**
`/etc/cc-ci` is a **manual `git clone`**, not nix-generated — `nix/hosts/cc-ci-hetzner/configuration.nix:7`
documents `git clone --recursive https://git.autonomic.zone/recipe-maintainers/cc-ci.git /etc/cc-ci`. Nothing
regenerates `.git/config` on `nixos-rebuild switch`. Therefore:
- After rotating `GITEA_PASSWORD`, the **old** value persists verbatim in this 0644 file (inert, but it should
still be scrubbed — it is the value published at `14c7dee`).
- If the operator re-clones or re-embeds the **new** password in the remote URL, the 0644 exposure **returns**.
**Recommendation (operator, alongside the B-redfix-8 rotation):** point the remote at a credential-less URL
(`https://git.autonomic.zone/recipe-maintainers/cc-ci.git`) and authenticate via a `credential.helper` /
token file with `0600`, or at minimum `chmod 0600 /etc/cc-ci/.git/config`. Do not carry userinfo in the URL.
**Rotation blast radius is small** (checked, so this is not a reason to delay): `GITEA_PASSWORD` is consumed
only by `scripts/bootstrap-drone-oauth.sh` (a one-off bootstrap). `scripts/recipe-mirror-sync.sh` pushes with
an **OAuth token**, not the password, so the weekly sweep's mirror sync does **not** depend on it.
**CORRECTION (wake #55): the "small blast radius" reasoning is HALF WRONG, and the OAuth token is ALSO
exposed.** The above argues rotation is safe because mirror-sync uses a token, not the password — but that
same `oauth2` token (`recipe-mirror-sync.sh:39`, `https://oauth2:${TOKEN}@…`, PUSH-capable) is itself leaked
in **117** world-readable `.git/config` copies under `/var/lib/cc-ci-runs` (sentinel `9c44a1aea2ecb389`;
verified live: `GET /api/v1/user` → 200 `autonomic-bot`/id 64). So there are **two** live push-capable
credentials to rotate, and rotating `GITEA_PASSWORD` alone leaves the token exposed. The token rotation DOES
have a real dependent (mirror-sync) — so it must be re-minted and the new value delivered to mirror-sync's
secret source, not merely revoked. See STATUS-redfix.md steps 34 (updated) for the combined remedy.
### A-redfix-1 — ADDENDUM (wake #53, 2026-07-09T10:15Z): the "sole copy" claim is FALSIFIED — **78** world-readable copies, root-caused to a git `insteadOf` rewrite that regenerates them on every CI run
The original finding asserted (see above, line ~47): "`/etc/cc-ci` is the **sole** copy whose parents are
world-traversable." I disbelieved that universal and probed it cold on cc-ci. **It is false.** Enumerating
every cred-bearing `.git/config` and testing each for uid-1000 readability (extract-then-hash, never echo):
ssh cc-ci 'find / -xdev -name config -path "*/.git/*" 2>/dev/null | while read f; do
grep -q "autonomic-bot:" "$f" || continue
setpriv --reuid=1000 --regid=1000 --clear-groups cat "$f" >/dev/null 2>&1 && echo "$f"; done'
# → 78 world-readable copies: 68 under /var/lib/cc-ci-runs/*/abra/recipes/*/.git/config,
# 8 in /nix/store/*-source/.git/config (0444, read-only fs), /tmp/v/.git/config, /etc/cc-ci/.git/config
All 78 carry the **same** live credential — `sha256(pw)[:16] = 3fcea78925015fc9`, identical to the B-redfix-8
sentinel (empty-input control `e3b0c44298fc1c14` to prove the extractor fires). So the exposure is **78×**, not
1×, and `/etc/cc-ci` is not special.
**Root cause (grep the callee, not the value).** `run_recipe_ci.py:360` clones a **clean, credential-less**
URL (`https://git.autonomic.zone/{src}.git`). The userinfo is injected by a global git rewrite in
`/root/.gitconfig`:
url.https://autonomic-bot:<pw>@git.autonomic.zone/.insteadOf = https://git.autonomic.zone/
Every per-run recipe clone the harness makes therefore bakes the cleartext password into its `.git/config` at
mode **0644**, under `/var/lib/cc-ci-runs/<rid>/abra/recipes/<recipe>/` (world-traversable parents). This is
the **generator**: the 68 run-dir copies accrete one-per-recipe-per-run and **regenerate autonomously on the
next CI run** — not just on a manual re-clone as the prior addendum supposed.
**Consequence for the remedy.** The prior recommendation (`chmod 0600 /etc/cc-ci/.git/config`, or delete
`/etc/cc-ci`) fixes **1 of 78** and does nothing about regeneration. A durable operator fix must remove the
credential from git's URL layer, e.g.:
- replace the `insteadOf`-with-userinfo rewrite in `/root/.gitconfig` with a `credential.helper` (token in a
`0600` file), so harness clones carry **no** userinfo; and
- scrub the existing run-dir + `/etc/cc-ci` + `/tmp/v` copies (the `/nix/store` ones are 0444 on a read-only
store and clear on GC).
Otherwise the next `!testme`/nightly-sweep run re-exposes the (even freshly-rotated) value at 0644.
**Scope note — NOT a gate, NOT a VETO, DONE stands.** The dashboard "no secrets" invariant was independently
**re-verified holding** this wake: `/runs/<rid>/abra/recipes/<recipe>/.git/config` → **404** on the live
dashboard (blocked by the `len(parts)==2` guard + the `_RUN_FILES` allow-list; positive control
`/runs/985/results.json` → 200, `has_cred=False`). This exposure is **host-local filesystem only** (any local
uid, incl. containers sharing the host mount), which is squarely the operator-only A-redfix-1/B-redfix-8
territory. It **widens** their remediation scope; it does not reopen any D-gate or DoD item.
### A-redfix-1 — ADDENDUM (wake #55, 2026-07-09T10:4xZ): wake-#53 `insteadOf` root cause RETRACTED; real generator = the sweep's `CCCI_SKIP_FETCH` copytree; a SECOND credential (oauth2 token) also exposed
Adjudicated the Builder's wake-#54 rebuttal cold. Corrections to my own wake-#53 addendum above:
- **RETRACT the `insteadOf` root cause.** Reproduced first-hand on the node (git 2.47.2): a clone
whose transport `url.<t>.insteadOf`-rewrites still stores the **original** URL; `insteadOf` injects
no userinfo when the clone URL is credential-less. My "regenerates via insteadOf" was wrong.
- **Real generator:** canonical `/root/.abra/recipes/*/.git/config` carry the cred (safe at rest under
`/root` = `0700`); `run_recipe_ci.py:348-353` `CCCI_SKIP_FETCH=1` `copytree`s them into
`/var/lib/cc-ci-runs` (`0755`), dropping the protection.
- **The exposure IS autonomously regenerated — weekly, by the sweep** (correcting the Builder's
"manual-* = hand-runs" claim). `run_id()`→`"manual"` for any non-Drone run; `nightly_sweep.py:88`
runs `run_recipe_ci.py` outside Drone with `CCCI_SKIP_FETCH=1`, so the sweep's runs are the
`manual-<pid>` dirs. Freshest copies dated 2026-07-05 03:3703:59Z = the sweep's last fire.
- **SECOND credential found (Builder census missed it).** `/var/lib/cc-ci-runs` holds **123**
world-readable cred-bearing configs: **68** `autonomic-bot/3fcea78925015fc9` (B-redfix-8 password)
**+ 55** `oauth2/9c44a1aea2ecb389` (a distinct OAuth2 token, sourced from bluesky-pds,
custom-html-tiny, gitea, mumble). Remedy must scrub/rotate **both**.
Still **OPEN**, still **operator-scope**, **no gate / no DoD impact, no VETO**. Endorse the Builder's
proposed remedy (strip userinfo from canonical origins + scrub + `chmod 0750 /var/lib/cc-ci-runs`)
with the two amendments above (both credentials; canonical-origin fix is required because a one-time
scrub is re-armed by the next sweep). Full reasoning + reproductions in REVIEW-redfix.md wake #55.
### B-redfix-8 — ADDENDUM (wake #58, 2026-07-09): exposure is **two published commits**, not one; and the `oauth2` token is **not** in git history
Triggered by the Adversary's wake-#57 break-it probe (`5431252`), which verified the token does not leak to
the dashboard / weekly reports / Drone log API — but did **not** probe the **public git mirror**, which is the
surface B-redfix-8 is actually about. Closing that gap surfaced a defect in my own STATUS.
**Method.** Exhaustive scan of the whole object db (`git cat-file --batch-all-objects` → **3099 blobs**),
literal-value match, `awk`-free (no `awk` on the orchestrator; an `awk`-based pipeline silently yields an
empty blob list and a **vacuous** `0 hits`). Token pulled root-only from a world-readable run-dir
`.git/config` into a `chmod 600` file, verified `sha256[:16]=9c44a1aea2ecb389` (matches the Adversary's
wake-#55 sentinel), `shred`-removed after. Positive control: the known-leaked password **must** be found.
**Results.**
- **password** (`sha16 3fcea78925015fc9`) → **2** blobs / **2** published commits:
`14c7dee`(blob `fd21fcb8`, 33408 B) and `223cc16`(blob `bcc31b55`, 34353 B). Both are ancestors of
`origin/main` ⇒ both mirror-served. Neither reachable from HEAD. `e99e2b3` and HEAD are clean.
⇒ STATUS's "**only** the historical commit `14c7dee` serves it" was **wrong and understated the exposure**.
A history scrub targeting `14c7dee` alone would leave `223cc16` serving the live credential. STATUS corrected.
- **oauth2 token** (`sha16 9c44a1aea2ecb389`) → **0** of 3099 blobs. The 271 blobs matching the *string*
`oauth2:` are `recipe-mirror-sync.sh:39` `https://oauth2:${TOKEN}@…` variable interpolation and machine-docs
prose citing the sha16 sentinel — no literal value.
**Consequence for the operator remedy.** The two credentials are **not symmetric**:
- **password** — exposed on the **public mirror** (permanent, unauthenticated, in every clone) **and** on the
filesystem. Rotation is urgent; scrubbing must cover **both** commits, and cannot recall existing clones.
- **oauth2 token** — exposed on the **filesystem only** (world-readable `.git/config` copies, A-redfix-1),
regenerated weekly by the sweep. Rotation + `chmod`/userinfo-strip suffices; no history rewrite needed.
**Guard for future probes.** Any scan of this repo reporting **0** password-bearing blobs is a **broken probe**
(missing `awk`, empty sentinel `e3b0c44298fc1c14`, or names-only grep of tracked paths). The correct answer
is **2**. Always run the positive control.
Still **operator-scope, outside DoD, no gate impact.** `## DONE` stands; no VETO.
### B-redfix-8 — ADDENDUM 2 (wake #58b, 2026-07-09): blob-count figure was brittle; the invariant is 2-and-0, not a total
The Adversary's wake-#58 adjudication (`63f7001`) independently CONFIRMED both results (token `0` blobs,
password exactly `2` — `fd21fcb8@14c7dee` + `bcc31b55@223cc16`, both mirror-served ancestors) — but reported
its total as **3061 blobs** where I reported **3099**. Neither is wrong; the *total* is simply not an invariant.
Root-caused, three independent sources of drift:
1. **New commits add blobs.** Same host now reads **3104** (my wake-#58 commits landed after the scan).
2. **`--batch-all-objects` counts unreachable objects** that `git rev-list --all` omits — **49** of them here.
Reachable-only is **3026**.
3. **`git clone /local/path` hardlinks the entire object store**, so a *local* clone inherits those unreachable
objects (verified: my cold clone also reads 3104). A clone from the **remote** gets only reachable objects —
which is why the Adversary's number is lower, and why it is the right scope for a *mirror-exposure* question.
**Consequence.** My STATUS quoted `# → 3099 lines` as the expected output of the documented reproduction. A
future agent running that command will see a different number, and could read the mismatch as either a broken
probe or a changed repo — both wrong. STATUS corrected: the reproduction now asserts `wc -l > 100` as a sanity
floor and names the **durable invariants — password ⇒ exactly 2 blobs, token ⇒ exactly 0** — which held across
all four scans (3099 / 3104 / 3026 / 3061) precisely because they are scope-independent.
Lesson, same shape as the `awk` near-miss: **an incidental figure recorded as if it were an assertion becomes a
future false alarm.** Record the invariant, not the measurement.
No new exposure, no gate impact, operator-scope. `## DONE` stands; no VETO.

View File

@ -1609,3 +1609,52 @@ trim job observed → adequate; revisit only if a cap is ever needed.
`dashboard/dashboard.py` + `tests/unit/test_dashboard.py` would be reformatted — confirmed pre-existing
at HEAD f68f1c5, outside the settings diff. Flagged for the dashboard owner / orchestrator; not fixed
here (narrow scope).
## Warm state is keyed by STACK NAMESPACE, not by recipe (phase redfix, F-redfix-4, 2026-07-09)
**Settled.** `/var/lib/ci-warm/<ns>/` is a slot owned by exactly ONE deployed stack. `ns` comes from
`canonical.canonical_ns(recipe)` for the data-warm canonical and `warmsnap.live_slot(recipe)` for the
live-warm reconciler. A recipe that is both (only `keycloak` today, via `warm.WARM_DOMAINS`) gets
`canon-<recipe>` for its canonical, so the two never share a slot. The canonical's **domain and its slot
derive from the same `canonical_ns()`**, so they cannot drift apart — that coupling is the invariant, not
the `canon-` string. Every slot `<ns>` maps 1:1 to the stack at `warm.stable_domain(<ns>)`.
Rejected alternatives:
- **Skip `seed_canonical` for `WARM_DOMAINS` recipes.** Silently de-enrolls keycloak and re-opens the DoD
item ("keycloak enrolled … verified green"). The Adversary named this as not acceptable; agreed.
- **Key every slot by the full domain / stack name** (the "purest" model). Correct, but it relocates the
reconciler's `last_good` for keycloak+traefik and `canonical.json` + `snapshot/` for 15 existing
canonicals on a live node, requiring a migration shim for a phase whose mandate is to fix red, not
re-architect. The chosen scheme is the same model applied only where two stacks actually contend, and
needs no migration (keycloak's canonical had never been seeded).
- **Guard-only (`_assert_slot_not_foreign` alone).** Turns silent destruction into a loud failure but leaves
keycloak's canonical unable to ever seed. Kept as defence in depth BEHIND the slot separation, not as the
fix.
Consequence for future enrollments: adding a recipe to `warm.WARM_DOMAINS` automatically namespaces its
canonical on both axes. `prune_stale()`'s "reconciler dirs are never pruned" invariant is now structural
(a `<recipe>/` dir never gains a `canonical.json`) rather than an incidental property that de-enrollment
could falsify.
## 2026-07-10 — Gitea credential rotation + git auth moved to SSH (operator-directed)
Both leaked `autonomic-bot` credentials were rotated and the exposures scrubbed (was B-redfix-8 /
A-redfix-1 / B-redfix-9, previously operator-scope):
- **Account password** rotated (old value now returns HTTP 401). It had been world-readable on the
public mirror in commits `14c7dee` + `223cc16`. History was rewritten (`git filter-repo --replace-text`)
to redact it and force-pushed; `main` went `bfa5396 -> 0d8adba` (tree byte-identical, history-only).
All clones + node `/etc/cc-ci` were reset to the rewritten `main`. Backup of pre-rewrite repo saved at
`~/cc-ci-backups/pre-scrub-*` (mirror + bundle). **Caveat:** the orphaned commits are still fetchable
by exact SHA on the public mirror until git.autonomic.zone runs a site-admin GC (value is dead, so inert).
- **`bridge_gitea_token`** (the oauth2 token, sops `secrets.yaml`) rotated: fresh PAT minted, re-encrypted,
deployed via `nixos-rebuild ?submodules=1`, old token + all 8 other stale PATs revoked. Only one PAT
remains: `cc-ci-bridge-*` (id 36).
**Git auth is now SSH, not HTTP.** git.autonomic.zone's Gitea only honours *preemptive* HTTP Basic auth;
credential-helper (challenge-response) always 401s on private repos — do not rely on it. Remotes are SSH
(`git@git.autonomic.zone:...`, port 2222 via `~/.ssh/config`); dedicated keys `cc-ci-orchestrator-20260710`
(orchestrator, `~/.ssh/autonomic-bot-gitea-ed25519`) and `cc-ci-node-20260710` (node, `/root/.ssh/...`).
`.gitmodules` secrets URL is SSH. `recipe-mirror-sync.sh` pushes over SSH (no token written into recipe
`.git/config` -> fixes the copytree leak). The bot **password stays in `/srv/cc-ci/.testenv` for API use only**
(comments / PR / repo creation); the weekly Thursday upgrade run picks it up by sourcing `.testenv`.
Node run-tree `/var/lib/cc-ci-runs` chmod 0750; 144 world-readable cred-bearing config copies scrubbed.

View File

@ -427,3 +427,29 @@ reachable via the operator/dev STAGES escape — production drone runs always ru
key on stdout (write to a run-scoped sidecar the test reads), or register the minted key in the harness
redaction set so even the RAW log is scrubbed. Low priority (RAW log is access-controlled; key is ephemeral).
- **Filed by:** Builder, phase prevb (acknowledging Adversary [F-prevb-C]).
## B-redfix-5 — warm reconciler's rollback `restore()` is outside the upgrade's `try/except`
- **What:** in `runner/warm_reconcile.py`, **two** post-`abra.undeploy()` warmsnap calls sit OUTSIDE the
`try/except` guarding the upgrade — **(a)** the upgrade path `:512-514` (`undeploy``wait_undeployed`
`warmsnap.snapshot(...)`) and **(b)** the unhealthy-rollback path `:534-536` (`undeploy`
`wait_undeployed``warmsnap.restore(...)``deploy_version(last_good)` at `:537`). If either raises for
any reason (absent/corrupt snapshot, foreign slot, docker error, "no volumes found") the exception
propagates, the following `deploy_version` never runs, and live keycloak is left **undeployed**, taking
down the shared OIDC provider `lasuite-*`/`drone` depend on. Site (a) fires on the *normal* upgrade path —
no rollback required.
- **Why deferred, not fixed:** structural gap, present at `07fc6d4` and predating the keycloak enrollment.
Not part of F-redfix-4's published clearing condition; the Adversary explicitly concurred it is **not a
VETO** and was "correctly filed rather than silently fixed".
- **Reachability, corrected (wake #42).** The earlier text here said F-redfix-4 "supplied the only
*reachable* trigger and that is now closed". That is wrong in one direction: `b5f2b10` *adds* a raise path
`_assert_slot_not_foreign()` is called inside BOTH `warmsnap.snapshot()` (`:158`) and `warmsnap.restore()`
(`:209`). What makes the composition unreachable **today** is that cc-ci's live slot
`/var/lib/ci-warm/keycloak/` holds no `snapshot/meta.json` at all (`read_meta``None`; an unclaimed slot
is free to claim) — not the closure of F-redfix-4. The one state that writes a foreign domain into the
live slot is a **pre-fix (`07fc6d4`) canonical seed**. Recorded as a MERGE PRECONDITION in STATUS-redfix.md.
- **Needed from operator:** decide the safe behaviour for a DB-backed app after a forward migration —
(a) redeploy `last_good` anyway even though data was not restored, or (b) die loudly + leave a breadcrumb
rather than start on unrestored data. That trade-off is a real safety call, not a mechanical fix, which is
why it was not settled inside a remediation commit.
- **Filed by:** Builder, phase redfix (from Adversary F-redfix-4 consequence 3).

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,456 @@ gitea, keycloak) → isolate → root-cause → classify (flake vs genuine; reci
warm-machinery vs load) → FIX each (recipe PR or harness improvement) → verify green. No standing
exceptions. Nothing merged.
## DONE — 2026-06-18T07:09Z
## DONE — 2026-07-09T00:18Z
Phase `redfix` COMPLETE. All six canon-sweep failures investigated in isolation, root-caused, classified,
**FIXED — each via a recipe PR or a harness improvement — and verified green**; no recipe left as a standing
exception; nothing merged (operator merges). Every gate has a fresh Adversary PASS in REVIEW-redfix.md and
there is **no standing VETO**:
- **M1 PASS** @ 2026-06-18T01:18Z (investigation/classification cold-verified).
- **M2 PASS** @ 2026-07-09T00:18Z (F-redfix-4 remedy cold-verified; VETO CLEARED; supersedes the
2026-06-18T07:06Z M2 PASS, which was given against parent `07fc6d4`).
Adversary findings F-redfix-1/2/3/4 are all **CLOSED**; no open blocking finding.
The four `A-redfix-*` findings are also non-blocking and are accounted for here, so nothing dangles:
- **A-redfix-1** (`/etc/cc-ci/.git/config` is 0644 and embeds the Gitea bot password) — **OPEN**, out of redfix
scope, pre-existing undeclared server state, no DoD item. Mirrors my B-redfix-7. Not created by this phase.
**It is the SAME credential as B-redfix-8, not merely a similar one** — verified first-hand 2026-07-09T09:1xZ:
the password extracted from the `.git/config` remote URL and the `GITEA_PASSWORD` in the orchestrator's
`/srv/cc-ci/.testenv` both hash to `3fcea78925015fc9` (neither is the empty-string artifact
`e3b0c44298fc1c14`). **Two exposures, one credential, still unrotated.** Folded into the B-redfix-8 remedy
below, because rotating the password does **not** scrub this file.
- **A-redfix-2** (the B-redfix-5 wedge's self-heal mechanism) — **PARTIALLY WITHDRAWN by the Adversary at
wake #44.** Its first half stands (a code path *does* redeploy keycloak — the fresh-deploy branch). Its
second half ("no re-trigger; heals on reboot") is **false** and is superseded by A-redfix-4.
- **A-redfix-3** (the `main.go` "both clones" evidence) — **ACCEPTED**; that evidence is retracted below.
`/srv/cc-ci` is a symlink to `/srv/cc-ci-orch`. The file's origin remains unexplained; risk inert.
- **A-redfix-4** (a weekly `nightly-sweep.timer` *does* re-trigger reconcile; the wedge is **silent** and
**recurring**) — **ACCEPTED and independently re-verified against the node**, not taken on authority. The
B-redfix-5 precondition below is rewritten accordingly: two unfalsifiable probes deleted, the wedge
described as a weekly alternating DOWN/UP outage that raises no alert, and the precondition re-anchored
from "slot clean at merge time" to **"never deploy `07fc6d4`"**. The trap is **not armed** and **cannot be
armed by autonomous node activity** (the tree the sweep runs has `WARM_CANONICAL = False`).
**MECHANISM CORRECTED (wake #54; Adversary REVIEW wake #54, re-verified first-hand by the Builder).** The
sweep does **not** run `origin/main`. `nightly-sweep.service` ExecStart is a nix-store wrapper that sets
`CCCI_REPO=/etc/cc-ci` and execs `cc-ci-run "$CCCI_REPO/runner/nightly_sweep.py"`, so the tree that
executes is the **deployed checkout `/etc/cc-ci`** (branch `main` @ `d11f8f5`, clean). The guarantee is a
property of **what is deployed to `/etc/cc-ci`**, so a future **deploy** — not a merge to `origin/main`
is what could arm the wedge. Conclusion unchanged (trap disarmed); only the named tree was wrong. Verify:
ssh cc-ci 'git -C /etc/cc-ci rev-parse --abbrev-ref HEAD; grep -n "^WARM_CANONICAL" /etc/cc-ci/tests/keycloak/recipe_meta.py;
git -C /etc/cc-ci merge-base --is-ancestor 07fc6d4 HEAD && echo ARMED || echo disarmed'
# EXPECTED: main / 16:WARM_CANONICAL = False / disarmed (a real assignment, not the line-15 comment)
All four corrected operator-facing *descriptions*, not DoD items. No VETO at wake #43 (`b358b7e`) or wake #44
(`84a1666`, 2026-07-09T08:47Z). **Note on provenance:** A-redfix-2's false half entered this file at `f64d102`
because I adopted the Adversary's stated mechanism *and its two probe commands* without re-deriving them. The
probes returned the all-clear for a reason unrelated to the property under test (they grep the app name;
the timer is named `nightly-sweep`). Both are now deleted, and every claim in the rewritten block below was
re-checked first-hand.
## ⚠ OPERATOR ACTION REQUIRED — B-redfix-8 (HIGH, open, outside DoD)
Phase DoD is met, but **one HIGH item is open and only an operator can close it.** It does not block `##
DONE` (no DoD item depends on it) and is recorded in full in BACKLOG-redfix.md.
**WHAT.** The live Gitea bot password (`GITEA_PASSWORD` in `/srv/cc-ci/.testenv`) was committed to
`machine-docs/BACKLOG-redfix.md`, pushed to `origin/main`, and is served **in cleartext to the
unauthenticated public internet** by the public mirror. It is push-capable to `recipe-maintainers/*`. HEAD
and the redaction commit `e99e2b3` are clean. Redaction cannot unpublish it — the value is permanent in
history and in every clone.
**CORRECTED (wake #58, 2026-07-09):** an earlier version of this entry said "**only** the historical commit
`14c7dee` serves it." That **understated the exposure**. An exhaustive scan of every blob in the object db
(`git cat-file --batch-all-objects`, literal-value match) finds the password in **two distinct
blobs**, carried by **two published commits** — both ancestors of `origin/main`, so both are mirror-served:
| commit | blob | `machine-docs/BACKLOG-redfix.md` size |
|---|---|---|
| `14c7dee` | `fd21fcb8ac02bff9917531c8175a29e450c41b19` | 33408 |
| `223cc16` | `bcc31b55860f782056ba3ccdf13165191bd329b5` | 34353 |
Neither blob is reachable from HEAD. Scrubbing only `14c7dee` would leave `223cc16` serving the credential.
**WHERE.** `git.autonomic.zone/recipe-maintainers/cc-ci`
`/raw/commit/14c7dee/machine-docs/BACKLOG-redfix.md` **and**
`/raw/commit/223cc16/machine-docs/BACKLOG-redfix.md`
**HOW to confirm exposure** (unauthenticated fetch; a raw blob at a fixed sha is immutable, so served size
must equal object size):
git cat-file -s 14c7dee:machine-docs/BACKLOG-redfix.md # → 33408
git cat-file -s 223cc16:machine-docs/BACKLOG-redfix.md # → 34353
An unauthenticated fetch of **either** raw URL returning **HTTP 200** at the matching byte count is the
leak, not an error page.
**Exhaustive-scan reproduction** (names/counts only; never prints the value). The `awk`-free form matters —
`awk` is absent on the orchestrator and an `awk`-based blob list yields an **empty** list, i.e. a vacuous
`0 hits`:
git cat-file --batch-all-objects --batch-check='%(objecttype) %(objectname)' \
| grep '^blob ' | cut -d' ' -f2 > /tmp/blobs # assert wc -l > 100 before trusting
# then literal-match each blob against a chmod-600 sentinel; positive control MUST return 2
**Do not assert an exact total blob count** — it is host- and time-dependent, and is *not* the invariant.
Observed legitimately: **3099** (Builder wake-#58 scan), **3104** (same host, after that wake's commits),
**3026** (reachable-only), **3061** (Adversary's cold clone, wake #58). Three sources of drift: (a) new
commits add blobs; (b) `--batch-all-objects` counts **unreachable** objects (49 here) that `git rev-list`
omits; (c) `git clone /local/path` **hardlinks the whole object store**, so a local clone inherits those
unreachable objects while a clone from the *remote* receives only reachable ones. All four scans agree on
the only figures that matter.
**Positive control is mandatory.** The durable invariants, stable across every scan scope above:
**password ⇒ exactly 2 blobs; token ⇒ exactly 0.** Any scan of this repo that reports **0** password-bearing
blobs is a **broken probe**, not a clean repo.
**The `oauth2` token (the second exposed credential) does NOT appear in git history.** Same exhaustive scan,
same run, literal match against the live token (`sha256[:16]=9c44a1aea2ecb389`, extracted root-only to a
`chmod 600` file, then `shred`-removed): **0 blobs** — independently reproduced by the Adversary at wake #58
against its own cold clone. The 271 history blobs matching the string
`oauth2:` are `scripts/recipe-mirror-sync.sh:39`'s `https://oauth2:${TOKEN}@…` **variable interpolation**
plus machine-docs **prose citing the sha16 sentinel** — no literal value. So the token's exposure is
**filesystem-only** (A-redfix-1: world-readable `.git/config` copies); it is **not** on the public mirror.
The password's exposure is **both** filesystem **and** public mirror. The two credentials differ in blast
radius; the operator remedy must not treat them as symmetric.
**HOW to confirm it is still the live credential** (commits to the value without republishing it).
Run this **on the orchestrator**`/srv/cc-ci/.testenv` lives there, **not** on `cc-ci`:
python3 -c 'import hashlib,re;v=re.search(r"^GITEA_PASSWORD=(.*)$",open("/srv/cc-ci/.testenv").read(),re.M).group(1).strip().strip(chr(34)).strip(chr(39));print(hashlib.sha256(v.encode()).hexdigest()[:16])'
**EXPECTED.** Prints `3fcea78925015fc9` while still unrotated. **A different digest ⇒ rotated ⇒ `14c7dee`
is inert ⇒ close B-redfix-8** — *but only if the probe was sound.* Two known-bad probes yield a different
digest without any rotation, and must NOT be used to close this item:
- **`e3b0c44298fc1c14` is the empty-input tell, not a rotation.** Running the command over `ssh cc-ci`
fails to open `.testenv`, hashes the empty string, and prints `sha256("")[:16]` = `e3b0c44298fc1c14`
(check: `printf '' | sha256sum`). Fix the probe; do not close B-redfix-8.
- **Do not conflate the three digests.** `3fcea78925015fc9` = sha256(credential *value*)[:16] — the only
rotation sentinel. `1994fd8d…` = sha256(the whole served *blob*) — immutable, changes never.
`e3b0c44298fc1c14` = sha256(empty) — a broken probe.
Exposure confirmed at **value level** (Adversary, 2026-07-09T07:34Z): the currently-valid, push-capable
password appears **verbatim** in the publicly served body, not merely "a blob that once held a secret."
**REMEDY (operator).** Rotate the Gitea bot password, then update **both** places that hold it. The credential
is a Class-A1 external infra input (plan §4.4) — the Builder must not rotate or invent it. History rewrite is
not available to the Builder either (`--force` is forbidden by the standing rules), so rotation is the only
action that actually revokes the exposure.
1. Rotate the bot password in Gitea.
2. Update `GITEA_PASSWORD` in `/srv/cc-ci/.testenv` **on the orchestrator**.
3. **Rewrite `/etc/cc-ci/.git/config` on `cc-ci` (A-redfix-1) — rotation does NOT scrub it.** That file is
`644 root:root` and embeds the same password in the `origin` URL's userinfo. `/etc/cc-ci` is a **manual
`git clone`**, not nix-generated (`nix/hosts/cc-ci-hetzner/configuration.nix:7` documents the clone), so no
`nixos-rebuild` regenerates or scrubs it. Strip the userinfo from the URL rather than re-embedding the new
password — re-embedding recreates the exposure verbatim:
ssh cc-ci 'git -C /etc/cc-ci remote set-url origin \
https://git.autonomic.zone/recipe-maintainers/cc-ci.git && chmod 600 /etc/cc-ci/.git/config'
**CORRECTION (wake #54#55, Builder, 2026-07-09): `/etc/cc-ci/.git/config` is 1 of many world-readable
copies of TWO live credentials — step 3 alone fixes 1.** The "sole copy" claim above is **withdrawn**.
Scope re-derived first-hand at wake #55 (a full-FS, per-file census — my wake-#54 count of 78 was BOTH
undercounted AND single-credential; Adversary flagged both, wake #55). Two distinct live credentials, each
its own `sha256(pw)[:16]` sentinel (empty-input control `e3b0c44298fc1c14`):
| credential | sentinel | world-readable copies (uid-1000-readable) |
|---|---|---|
| `autonomic-bot` **password** (B-redfix-8) | `3fcea78925015fc9` | 78 (68 run-dir + 8 `/nix/store` 0444 + `/tmp/v` + `/etc/cc-ci`) |
| `oauth2` **Gitea token** (NEW, wake #55) | `9c44a1aea2ecb389` | 117 (`/var/lib/cc-ci-runs`), sourced from 20 canonical clones |
Per-file under `/var/lib/cc-ci-runs`: **62 carry BOTH**, 16 password-only, 55 token-only → **133 distinct
world-readable cred-bearing files** there. **The `oauth2` token is LIVE and PUSH-CAPABLE** — verified
`GET /api/v1/user` → 200 `login=autonomic-bot id=64`, and `recipe-mirror-sync.sh:39` pushes with it
(`https://oauth2:<TOKEN>@…`). So **both** secrets must be rotated, not just `GITEA_PASSWORD`. Re-census:
ssh cc-ci 'find / -xdev -name config -path "*/.git/*" 2>/dev/null | while read f; do
setpriv --reuid=1000 --regid=1000 --clear-groups cat "$f" >/dev/null 2>&1 || continue
grep -qE "autonomic-bot:|oauth2:" "$f" && echo "$f"; done | wc -l'
# EXPECTED: ~135 today (133 in /var/lib + /tmp/v + /etc/cc-ci); after remediation: 0 (ignoring 0444 /nix/store)
Legacy list (password only), retained for the step-3 command below — the `origin` in these carries the
password sentinel `3fcea78925015fc9`:
**68** under `/var/lib/cc-ci-runs/manual-*/abra/recipes/*/`, **8** in `/nix/store` (0444, read-only, clears
on GC), **1** `/tmp/v`, **1** `/etc/cc-ci`. Verify:
ssh cc-ci 'find / -xdev -name config -path "*/.git/*" 2>/dev/null | while read f; do
grep -q "autonomic-bot:" "$f" || continue
setpriv --reuid=1000 --regid=1000 --clear-groups cat "$f" >/dev/null 2>&1 && echo "$f"; done | wc -l'
# EXPECTED: 78 (after remediation: 0, ignoring /nix/store)
4. **Scrub the copies AND stop regeneration (A-redfix-1, widened).** The generator is the **credentialed
remotes in the canonical clones** `/root/.abra/recipes/*/.git/config` (password on `origin`, oauth2 token
on the `gitea`/mirror remote), which `run_recipe_ci.py:348-353`'s `CCCI_SKIP_FETCH=1` `shutil.copytree`
copies into the world-traversable run tree (`/var/lib/cc-ci-runs`, `0755`). Canonicals are shielded by
`/root` = `0700`; the run-dir copies are not.
**REGENERATES WEEKLY (corrected wake #55).** The `manual-*` run dirs are NOT hand-run leftovers: the
autonomous `nightly-sweep.timer` runs `run_recipe_ci.py` outside Drone (`nightly_sweep.py:88` sets
`CCCI_SKIP_FETCH="1"`), and `run_id()` labels any non-Drone run `manual-<pid>` (`run_recipe_ci.py:318-319`).
So the sweep re-creates these copies every fire (freshest copies dated 2026-07-05 03:3703:59Z = the
`LastTriggerUSec` 2026-07-05 03:04:50Z sweep). **A one-time scrub is re-exposed on the next sweep unless
the canonical origins are stripped or `/var/lib/cc-ci-runs` is hardened to `0750` durably.** Strip **both**
credentials at the source, then scrub the copies:
ssh cc-ci 'for d in /root/.abra/recipes/*/; do r=$(basename "$d");
for rem in origin gitea upstream; do
git -C "$d" remote get-url "$rem" >/dev/null 2>&1 || continue
git -C "$d" remote set-url "$rem" "$(git -C "$d" remote get-url "$rem" | sed -E "s#://[^@/]+@#://#")"; done; done
rm -rf /tmp/v
find /var/lib/cc-ci-runs -path "*/.git/config" \( -exec grep -lq "autonomic-bot:" {} \; -o -exec grep -lq "oauth2:" {} \; \) -delete
chmod 0750 /var/lib/cc-ci-runs'
Stripping the userinfo does **not** break the clone: the sweep only ever *fetches*, and the mirror serves
this repo anonymously (that is precisely what B-redfix-8 exploits). Verified 2026-07-09T09:1xZ —
`GIT_TERMINAL_PROMPT=0 git -c credential.helper= ls-remote https://git.autonomic.zone/recipe-maintainers/cc-ci.git HEAD`
`f0372b8… HEAD`, rc=0, no credential. The sibling `cc-ci-secrets` submodule URL (`.git/config:15`)
already carries no userinfo, so this matches the existing pattern.
**Independently cold-verified by the Adversary (REVIEW-redfix.md wake #46, `3f27cc6`), which attempted to
refute it three ways and failed on all three** — so this step is safe to run on the node:
- **No push through `origin`.** The only `git push` in the deployed tree (`scripts/recipe-mirror-sync.sh:84-85`)
`remote add`s a `gitea` OAuth-token remote inside a *recipe* clone; it never touches `/etc/cc-ci`'s origin.
- **The `secrets` submodule survives the strip.** Its URL already carries no userinfo and
`cc-ci-secrets.git` is anonymously fetchable (`ls-remote` rc=0), so `submodule update` still works.
- **Nothing auto-pulls the checkout.** `/etc/cc-ci` is clone-once + local `nixos-rebuild switch`; the sweep
never `git pull`s it. (`/root/.git-credentials` exists but no `credential.helper` is wired at global or
system scope, so it is inert — the anonymous `ls-remote` results above are genuinely credential-free.)
**Rotation blast radius is small — no operational reason to delay.** `GITEA_PASSWORD` is consumed by exactly
one script, `scripts/bootstrap-drone-oauth.sh` (`git grep -ln GITEA_PASSWORD -- .` returns that script, `docs/
install.md`, and machine-docs prose; nothing else in the runner). In particular `scripts/recipe-mirror-sync.sh`
pushes with an OAuth **token** read from `/run/secrets/bridge_gitea_token` (`:30`), not this password, so
mirror sync is unaffected by the rotation.
**HOW to confirm BOTH exposures are closed** (each prints a digest; a value other than `3fcea78925015fc9` on
both, and not the empty-string tell `e3b0c44298fc1c14`, means rotated):
# exposure 2 — the embedded copy; `python3` is absent on cc-ci, so use sha256sum
ssh cc-ci 'sed -n "s#^\s*url = https://[^:]*:\([^@]*\)@git\.autonomic\.zone/recipe-maintainers/cc-ci\.git#\1#p" \
/etc/cc-ci/.git/config | head -1 | tr -d "\n" | sha256sum | cut -c1-16'
**EXPECTED after step 3:** the command prints **nothing** (no userinfo left to match) — an empty result here
is the fix, whereas an `e3b0c44298fc1c14` from the *step-2* probe is a broken probe. Do not confuse them.
**Merge target: `redfix-m2-harness` @ `b5f2b10`** (not `07fc6d4` — that tip carries the F-redfix-4 defect).
The earlier `## DONE` of 2026-06-18T07:09Z was **withdrawn** on 2026-07-09 under the standing VETO
F-redfix-4 and is re-asserted here only after the remedy was Adversary-verified. Details of that cycle,
and the still-open non-blocking B-redfix-5, are below.
One deferred, non-blocking item remains recorded (NOT part of any DoD item, NOT a standing finding):
**B-redfix-5** in BACKLOG-redfix.md — in `warm_reconcile.py`, TWO post-`abra.undeploy()` calls sit outside
the upgrade's `try/except`: `warmsnap.snapshot()` on the upgrade path (`:514`) and `warmsnap.restore()` on
the rollback path (`:536`). If either raises, live keycloak is left **undeployed**, and because both sit
upstream of every `write_alert()` call site the failure is **silent**. Pre-dates the enrollment (present at
`07fc6d4`). Adversary concurred it is not a VETO. Its *consequence* is worse than first recorded — see the
merge precondition below: reached only via a foreign live-slot meta, which nothing on the node can create
today, but if ever reached it produces a weekly alternating SSO outage that reports `Result=success`.
**MERGE PRECONDITION for `b5f2b10`.** The F-redfix-4 fix *adds* a raise path to both of those calls —
`warmsnap._assert_slot_not_foreign()` (`warmsnap.py:158` in `snapshot()`, `:209` in `restore()`). It raises
iff the live slot's `snapshot/meta.json` records a `domain` other than the one passed. Composed with
B-redfix-5, a foreign live-slot meta wedges live keycloak: on the **upgrade** path this fires on *every*
stateful auto-upgrade, not only on rollback. It is **unreachable on cc-ci today** because the live slot holds
no snapshot at all (`read_meta``None` → "a slot with no snapshot yet is free to claim"). It is unreachable
for that reason — **not** because F-redfix-4 is closed.
**THE BINDING RULE: never deploy `07fc6d4`.** (Re-anchored 2026-07-09 per Adversary **A-redfix-4**; the
earlier "verify the slot is clean at merge time" framing named the wrong landmark — *a `git merge` executes
nothing*.) The only state that creates a foreign live-slot meta is a **canonical keycloak seed executed from
a tree that carries the enrollment but not `canonical_ns()`**. Exactly one such tree exists: `07fc6d4`
(`tests/keycloak/recipe_meta.py``WARM_CANONICAL = True`, no `canonical_ns()`). `b5f2b10` ships enrollment
**and** fix together, so its canonical seeds land in `canon-keycloak/` and the live slot never goes foreign.
The precondition is therefore **self-maintaining across the merge** — deploying the merge target cannot arm
the trap; only deploying the intermediate `07fc6d4` can.
**No autonomous node activity can arm it.** The tree the weekly sweep executes is `CCCI_REPO=/etc/cc-ci` =
`origin/main`, where `WARM_CANONICAL = False`. Only `07fc6d4` and `b5f2b10` have `True`.
**If it is ever armed, the wedge is SILENT and RECURRING — a weekly alternating outage of the shared SSO
provider.** (Corrected 2026-07-09 per **A-redfix-4**, which withdrew the "no re-trigger / heals on reboot"
half of A-redfix-2. My own earlier "recovery is manual" (`68b51d5`) was also wrong. Re-derived independently
against the node — see HOW below.) `warm_reconcile.py` has **two** invokers, and the second is a weekly timer:
nightly-sweep.timer (OnCalendar=Sun *-*-* 03:00:00, Persistent=true)
└─ nightly_sweep.main():147 → roll_warm_infra() → subprocess warm_reconcile.py keycloak
(WARM_APPS = ["keycloak", "traefik"], nightly_sweep.py:39,57-61)
Once armed, with `current != latest`: **sweep N**`abra.undeploy()``snapshot()` guard raises → keycloak
**DOWN**. **Sweep N+1** (7 days later) → `is_deployed` is `False` → the **fresh-deploy branch**
(`:471-479`), which **never calls `warmsnap`** → keycloak **UP** on the old version. **Sweep N+2** → upgrade
path → **DOWN** again. **Alternating weeks, indefinitely**, because the foreign `snapshot/meta.json` is never
removed. (`abra.undeploy()` (`abra.py:295-297`) runs only `abra app undeploy -n` and does **not** remove the
app `.env`, so `current_version` stays resolvable — this is why the fresh-deploy branch is the one taken.)
**And it raises no alert.** `roll_warm_infra()` captures the subprocess rc and discards it
(`nightly_sweep.py:59-62``rc` is only ever printed), so `nightly-sweep.service` still reports
`Result=success`. The raise at `:514` is outside the only `try/except` (`:520-525`, which wraps
`deploy_version` + `wait_healthy`*that gap is B-redfix-5*) and upstream of **every** `write_alert()` call
site (`:493,500,503,539`). `reconcile()`'s sole caller `main():556` does not catch either. A weekly SSO
outage would surface **nowhere**. (All line numbers here are `b5f2b10` coordinates, matching the rest of this
block; on `main` the same sites sit two lines earlier.)
**REMEDIATION if ever armed: delete the foreign `snapshot/` from the slot** (or fix B-redfix-5).
**Redeploying keycloak does NOT fix it** — the next sweep re-wedges. This is the one part of the old text
that was right, and it is the part that matters operationally.
**HOW to verify** (from a clone at `b5f2b10`). Note the probe greps the **callee**, never the app name: the
invoking timer is called `nightly-sweep`, so `list-timers | grep -i keycloak` and `git grep warm-keycloak --
nix/` are both empty **while a timer drives it weekly**. Those two probes were in this file until 2026-07-09
and are unfalsifiable by construction; they have been deleted, not fixed.
git grep -n warm_reconcile -- runner/ nix/ # SOUND probe: finds both invokers
git show redfix-m2-harness:runner/harness/abra.py | sed -n '295,297p' # undeploy: no .env removal
git show redfix-m2-harness:runner/warm_reconcile.py | sed -n '471,479p' # fresh-deploy branch, no warmsnap
ssh cc-ci 'systemctl cat nightly-sweep.timer | grep -iE "OnCalendar|Persistent"'
ssh cc-ci 'systemctl list-timers --all nightly-sweep.timer --no-pager'
**EXPECTED:** `git grep warm_reconcile` prints both `nix/modules/warm-keycloak.nix:26` (the oneshot unit) and
`runner/nightly_sweep.py:60` (the weekly sweep's subprocess); `undeploy()` is a single
`_run(["app","undeploy",domain,"-n"], …)` with no `.env` handling; `:472` is `if not deployed:` returning
`deployed-fresh:{target}` at `:479` with no `warmsnap` between; the timer prints
`OnCalendar=Sun *-*-* 03:00:00` + `Persistent=true`.
**Observational proof the sweep — not the unit — drives reconcile** (independent of reading any code;
observed 2026-07-09):
ssh cc-ci 'systemctl show warm-keycloak.service -p ExecMainStartTimestamp
stat -c "%y %n" /var/lib/ci-warm/keycloak/last_good
systemctl list-timers --all nightly-sweep.timer --no-pager'
**EXPECTED:** unit last ran `Wed 2026-06-17 17:29:31 UTC`; `last_good` mtime `2026-07-05 03:04:51.209`; timer
last trigger `Sun 2026-07-05 03:04:50 UTC`. The slot was written **18 days after the unit last ran and 1.2 s
after the timer fired**, and `write_last_good()` is reachable **only** from `reconcile()` (call sites
`:478,484,491,527`, all inside it) ⇒ `reconcile("keycloak")` executed from the sweep.
**Also:** `warm-keycloak.service`'s `ExecStart` resolves to `/nix/store/…-runner/warm_reconcile.py`, and
`harness/warmsnap.py` lives in that **same** derivation. So `nixos-rebuild switch` re-runs reconcile **iff
`runner/**` changed** — an unrelated switch heals nothing, while the merge's own activation switch *is* a
reconcile trigger (alongside the Sunday sweep).
**Blast radius: keycloak only.** `warm.WARM_DOMAINS == {"keycloak"}` and `keycloak` is the only `SPECS` entry
with `stateful: True` (`traefik` is `stateful: False` — version-rollback-only, never snapshots). No other
warm unit reaches either call site.
**HOW to verify the trap is not armed** (on cc-ci; `python3` is absent there, so `ls`/`cat`):
ssh cc-ci 'ls -A /var/lib/ci-warm/keycloak/; cat /var/lib/ci-warm/keycloak/snapshot/meta.json 2>&1'
**EXPECTED (observed 2026-07-09T08:5xZ):** `last_good` only, and `cat` reports `No such file or directory`
no `snapshot/`, hence no `meta.json`; no `/var/lib/ci-warm/canon-keycloak` either; live keycloak `200`. **A
`meta.json` whose `"domain"` is anything other than `warm-keycloak.ci.commoninternet.net` means the trap is
armed:** delete that foreign `snapshot/` (or fix B-redfix-5) **before the next Sunday sweep**, not merely
before the merge. A `meta.json` recording `warm-keycloak.ci.commoninternet.net` is self-consistent and passes
the guard.
---
### F-redfix-4 remedy (the reason DONE was withdrawn and re-asserted) — 2026-07-09
**WHAT.** F-redfix-4 is fixed at `redfix-m2-harness` @ **`b5f2b10`** (parent `07fc6d4`, the sha the M2 PASS
was given against). Warm state is now keyed by a **stack namespace**, not a bare recipe:
`canonical.canonical_ns(r)` is the single source from which BOTH the canonical's domain and its warm-state
slot derive. A live-warm provider (`r in warm.WARM_DOMAINS`) gets ns `canon-<r>` → domain
`warm-canon-keycloak.ci.commoninternet.net` (**unchanged**) + slot `/var/lib/ci-warm/canon-keycloak/`.
Every other recipe keeps ns `<r>` (the invariant is structural — `r not in warm.WARM_DOMAINS` — not a
property of any particular count). `WARM_DOMAINS == {"keycloak"}` is a singleton, so the re-key is provably
`keycloak -> canon-keycloak` and nothing else: of the 21 enrolled recipes the other 20 satisfy
`canonical_ns(r) == r`, so **zero existing canonical slots change**. On live disk that is 17 seeded
canonicals (those carrying a `canonical.json`), all of which remain in `prune_stale`'s `keep` set.
Addresses all four consequences: (1)+(2) slots disjoint, neither deployment can replace the other's
known-good; (3) the outage race is removed with the shared slot; (4) `prune_stale`'s reconciler-dir
invariant is now structural — `<recipe>/` never gains a `canonical.json`.
Also: `warmsnap._assert_slot_not_foreign()` refuses to snapshot/restore a slot recorded against a different
domain (defence in depth; fails before the destructive swap, not at the next restore). The two comments
asserting the deployments "can never touch each other" are corrected (`canonical.py`,
`tests/keycloak/recipe_meta.py`). `WARM_CANONICAL = True` is retained — keycloak stays enrolled, no
skip-guard, no silent de-enrollment.
**MIGRATION: none required.** On cc-ci `/var/lib/ci-warm/keycloak/` contains only `last_good` (the
canonical was never seeded), so no existing file changes slot. Verify: `ls -A /var/lib/ci-warm/keycloak/`
`last_good` only.
**WHERE.** cc-ci repo, branch `redfix-m2-harness` @ `b5f2b10`. Files: `runner/harness/warmsnap.py`,
`runner/harness/canonical.py`, `runner/warm_reconcile.py`, `runner/run_recipe_ci.py`,
`tests/keycloak/recipe_meta.py`, `tests/unit/test_warmsnap.py`, `tests/unit/test_canonical.py`.
**HOW to verify (1) — unit suite, cold clone, no docker needed.**
git clone -q --branch redfix-m2-harness <cc-ci remote> /tmp/v && cd /tmp/v
/nix/store/x188l04r3gfkh18gy1dpf05fv3kkrgs7-python3-3.12.8-env/bin/python3 -m pytest tests/unit -q
EXPECTED: `325 passed` (baseline at parent `07fc6d4` is `315 passed`; +10 F-redfix-4 regression tests).
The 10 include `test_live_and_canonical_slots_are_disjoint`, `test_slot_maps_1to1_to_its_stack`,
`test_registry_path_of_live_warm_provider_is_not_the_reconciler_dir`,
`test_prune_stale_deenrolled_provider_spares_reconciler_last_good`,
`test_snapshot_refuses_to_clobber_another_domains_slot`, `test_restore_refuses_a_foreign_slot`.
**HOW to verify (2) — the VETO's clearing condition, on the real node.** Non-destructive: writes only to a
scratch `CCCI_WARM_ROOT` + one throwaway docker volume. Uses the real idle `warm-canon-keycloak` stack for
the canonical side and a throwaway `warm-fakelive_…_data` volume as the live-warm stand-in (the live
`warm-keycloak` stack cannot be undeployed to snapshot it).
ssh cc-ci
docker volume create warm-fakelive_ci_commoninternet_net_data
MP=$(docker volume inspect -f '{{.Mountpoint}}' warm-fakelive_ci_commoninternet_net_data); echo x > $MP/live.txt
git clone -q --branch redfix-m2-harness <cc-ci remote> /tmp/v && mkdir -p /tmp/vw/keycloak
echo '10.7.1+26.6.2' > /tmp/vw/keycloak/last_good
cd /tmp/v/runner && CCCI_WARM_ROOT=/tmp/vw /nix/store/jag2131a95gw6ng7grig9pj3dn2q8vrv-python3-3.12.8-env/bin/python3 - <<'PY'
import sys; sys.path.insert(0, ".")
from harness import warmsnap as ws, canonical as c
LIVE, CANON = ws.live_slot("keycloak"), c.canonical_slot("keycloak")
CANON_DOM, LIVE_DOM = c.canonical_domain("keycloak"), "warm-fakelive.ci.commoninternet.net"
print(ws.snap_dir(LIVE), ws.snap_dir(CANON), c.registry_path("keycloak"), sep="\n")
ws.snapshot(CANON, CANON_DOM, version="canon-known-good")
ws.snapshot(LIVE, LIVE_DOM, version="live-last-good") # used to clobber the canonical
print("restore(canon) ->", ws.restore(CANON, CANON_DOM)["volumes"])
print("restore(live) ->", ws.restore(LIVE, LIVE_DOM )["volumes"])
print("last_good:", open("/tmp/vw/keycloak/last_good").read().strip())
try: ws.snapshot(CANON, LIVE_DOM); print("FAIL: foreign snapshot allowed")
except ws.SnapshotError as e: print("foreign REFUSED:", str(e)[:60])
PY
docker volume rm warm-fakelive_ci_commoninternet_net_data; rm -rf /tmp/vw /tmp/v
EXPECTED (observed 2026-07-09):
/tmp/vw/keycloak/snapshot <- live slot
/tmp/vw/canon-keycloak/snapshot <- canonical slot: DISJOINT
/tmp/vw/canon-keycloak/canonical.json <- registry NOT in the reconciler's dir
restore(canon) -> ['warm-canon-keycloak_ci_commoninternet_net_mariadb',
'warm-canon-keycloak_ci_commoninternet_net_providers']
restore(live) -> ['warm-fakelive_ci_commoninternet_net_data']
last_good: 10.7.1+26.6.2 <- survived the canonical seed
foreign REFUSED: warm slot 'canon-keycloak' holds the known-good of ...
Each `restore()` returns its OWN stack's volumes (the clearing condition). Pre-fix, the same script prints
one shared slot `/tmp/vw/keycloak/snapshot` for both and `restore(canon)` raises `SnapshotError`.
**Node integrity after my own run of the above (2026-07-09).** Real warm root untouched
(`/var/lib/ci-warm/keycloak/` = `last_good` only); `warm-canon-keycloak` mariadb digest byte-identical
across the restore round-trip (`1201440268 48846` before and after, 391 files / 2 files); live
`warm-keycloak…/realms/master` **200** throughout; throwaway volume removed; scratch removed.
**Scope.** keycloak only. M1 classifications and the discourse / mattermost-lts / gitea / bluesky-pds /
mumble fixes are untouched by this commit and remain Adversary-verified (M1 PASS @2026-06-18T01:18Z; the
five non-keycloak M2 fixes content-verified through re-confirmation #7).
**Known residual, NOT in F-redfix-4's clearing condition** (filed as B-redfix-5 in BACKLOG-redfix.md, not
blocking): `warm_reconcile.py`'s rollback `warmsnap.restore()` still sits outside the `try/except` that
guards the upgrade, so any restore failure (e.g. a corrupt/absent snapshot) leaves live keycloak undeployed
after `abra.undeploy()`. The F-redfix-4 race that made this reachable is gone; the pre-existing robustness
gap is not. Recorded, not silently dropped.
The prior DONE text is retained verbatim below as the historical record of what was claimed on 2026-06-18.
---
## (HISTORICAL — withdrawn 2026-07-09 under VETO F-redfix-4; superseded by the DONE above) DONE — 2026-06-18T07:09Z
Phase `redfix` COMPLETE. All six canon-sweep failures investigated in isolation, root-caused,
classified, **FIXED — each via a recipe PR or a harness improvement — and verified green**; no recipe
@ -26,6 +475,29 @@ M1 read-only crash gone; bluesky-pds recipe PR #4 @4987ba9 (caddy `${STACK_NAME}
200 (was 000). gitea/bluesky end-to-end canonical advance is operator-merge-gated (fix proven by
chaos-deploy; published tags don't carry it pre-merge) consistent with "nothing merged", not a shrug.
### Evidence addendum 2026-07-08 (re: F-redfix-3) — discourse sha pins have rotted; verify by CONTENT
The discourse shas cited below (`9ff5e19`, `53ba0910`) **no longer resolve on the mirror**. A *later* phase
force-pushed and extended the shared branch `discourse-official-image` (now `ede6399` = `refs/pull/5/head`;
redfix's PR is no longer `#4`). This is branch drift by later work, **not** a retraction of the redfix fix,
and it does not disturb the M2 PASS which was given against the shas that existed on 2026-06-18. The other
three recipe pins and the harness pin are exact and still resolve: mattermost-lts `ci/pg-restore`@`4ca7f418`,
gitea `ci/app-ini-writable`@`a0f2db88`, bluesky-pds `ci/warm-routing-alias`@`4987ba91`, cc-ci
`redfix-m2-harness`@`07fc6d4a`.
Historical sha lines below are left **unedited on purpose** they record what was verified when. Use this
durable **content assertion** instead of the shas to re-verify discourse at any future head:
git clone https://git.autonomic.zone/recipe-maintainers/discourse && cd discourse
git show origin/discourse-official-image:compose.yml | grep -m1 'image:.*discourse'
git show origin/discourse-official-image:compose.smtpauth.yml | grep -c sidekiq
EXPECTED: `image: discourse/discourse:3.5.3` (the official-image migration = M2's claim) and `0` (the
F-redfix-1 orphaned-sidekiq removal). Both re-confirmed by the Builder at `ede6399` on 2026-07-08, and by the
Adversary at `ede6399` and `refs/pull/4/head`@`0c4539b7`. (Caveat when reproducing absence of a sha: a
`--filter=blob:none` clone and `git fetch origin <sha>` both yield false "absent" signals test reachability
from all `refs/heads/*` + `refs/pull/*/head` instead.)
---
## Phase: M1 — investigate + isolate + classify (IN PROGRESS)
@ -235,6 +707,33 @@ in the two sections above ("M1 results table" and "HOW the Adversary cold-verifi
Evidence logs on cc-ci: `/tmp/redfix-{discourse,mattermost-lts,mumble,mumble2,bluesky-pds,gitea2}.log`.
Reasoning/dead-ends in JOURNAL-redfix.md. Node left clean (see "Node state left clean" above).
## Untracked file `main.go` — NOT produced by this phase (operator: unexplained)
**WHAT.** An untracked `main.go` (281 bytes, a Go "Hello, World!" `net/http` server on `:8080`) sits in the
repo root. It is not in any commit on any ref, is not gitignored, and is unrelated to every redfix DoD item.
No `go.mod` accompanies it. I did not create it and have not committed, modified, or deleted it.
**WHERE.** `/srv/cc-ci-orch/cc-ci/main.go`**one file in one clone.**
**RETRACTED (2026-07-09, Adversary A-redfix-3):** an earlier version of this entry claimed the file was
"present in **both** clones … i.e. written by something outside git." **That evidence is void.** `/srv/cc-ci`
is a **symlink to `/srv/cc-ci-orch`**, so `/srv/cc-ci/cc-ci/main.go` and `/srv/cc-ci-orch/cc-ci/main.go` are
the **same inode** — one file seen twice, not two clones. The "a git operation cannot leave the same untracked
file in two clones" inference therefore carries **no information** about the file's origin, which remains
**unexplained**. Independently re-derived: `ls -la /srv/` shows `cc-ci -> /srv/cc-ci-orch`, and
`stat -c '%i %h %n'` on both paths prints the same inode `3254604` with `links=1`.
**HOW to verify.** `git log --all --oneline -- main.go` → empty (in no commit).
`git status --short``?? main.go`.
`ls -la /srv/ | grep cc-ci``cc-ci -> /srv/cc-ci-orch` (the symlink that voids the "both clones" claim).
`stat -c '%i %h %n' /srv/cc-ci/cc-ci/main.go /srv/cc-ci-orch/cc-ci/main.go` → same inode, `links=1`.
**Risk: inert** (Adversary-confirmed). 281-byte hello-world `net/http` listener; no `go.mod`; `go` is **not
installed**; **nothing listening on `:8080`**; in no commit on any ref; unreferenced by any tracked file.
**EXPECTED.** Left in place, untracked. Per guardrails I do not delete or commit files I did not create;
flagging for the operator rather than acting. It affects no gate, no DoD item, and no verdict.
## Blocked
(none)

View File

@ -36,7 +36,10 @@ RECIPE_DIR="${HOME}/.abra/recipes/${RECIPE}"
}
TOKEN="$(tr -d '[:space:]' <"${TOKEN_FILE}")"
API="https://${GITEA_HOST}/api/v1"
MIRROR_PUSH="https://oauth2:${TOKEN}@${GITEA_HOST}/${NAMESPACE}/${RECIPE}.git"
# Push over SSH (git@:2222, key in ~/.ssh) so no credential is ever written into the recipe
# clone's .git/config — that config gets copytree'd into the world-readable per-run tree
# (CCCI_SKIP_FETCH staging), which previously leaked the token. API still uses the token below.
MIRROR_PUSH="git@${GITEA_HOST}:${NAMESPACE}/${RECIPE}.git"
auth=(-H "Authorization: token ${TOKEN}")
# Ensure the recipe is cloned (abra recipe fetch clones from the catalogue → upstream).

Submodule secrets updated: cdd5e0ad25...2ce5f86c02