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
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
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.
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.
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
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
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
Per-recipe history now sources the full run list from local /var/lib/cc-ci-runs
artifacts; deployed (image 11ac2a1e6c07, 1/1) + verified live: bluesky-pds 8 in
exact host ts order, ghost 24/immich 28/discourse 25, plausible/custom-html
capped 30 newest; overview+badges 200; traversal/injection rejected; retention
no-trim. DoD plan §5 met.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
history_for() now enumerates run dirs' results.json, groups by recipe, sorts
newest-first by finished timestamp (mixed numeric+named ids — timestamp is the
only correct key), caps at HISTORY_CAP=30, skips malformed/empty/no-recipe dirs.
Overview + badges + /runs + security guards + stdlib-only unchanged.
Local verify: 13/13 unit tests; full-fixture vs 308 real results.json →
bluesky-pds=8 in exact ts order, plausible capped 30 newest, edge dirs skipped.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Orchestrator-written marker: the Builder hit the opus usage limit and could not
write its own DONE. Work is complete + Adversary-verified (M1 1310a95, M2
199f5b6, cleared for DONE). Unblocks auto-advance to canon.
The previous/ base-repair mechanism exists and can be used when updating tests
if a previous base won't deploy, but it is explicitly a last resort: reach for
it only after the dynamic base (last-green -> main-tip) fails to come up, since
each previous/ re-introduces the per-version patching treadmill the dynamic
base removed. Most recipes (incl. discourse) need none.
The cc-ci server already runs on Hetzner (migration done; nix/hosts/cc-ci-hetzner
landed directly on main 2026-05-31). PR#2's host config was superseded by newer
main commits, but its terraform/ provisioning scaffolding (cpx32 + nixos-infect)
was never preserved. Add it here as the infrastructure-of-record so the box is
reproducible. .gitignore keeps tfstate + secret tfvars out; HCLOUD_TOKEN is an
env var at apply time (no secrets committed). PR#2 closed as superseded.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Complete reference for per-recipe CI customization: all 18 recipe_meta keys
(incl. the base pin UPGRADE_BASE_VERSION), the six divergent meta loaders,
every hook file (test_<op>.py overlays, ops.py, install_steps.sh,
setup_custom_tests.sh, compose.ccci.yml), env contracts, and §8 known
limitations / restructuring candidates (R1 loader drift, R2 dead SCREENSHOT
knob, R6 silent-typo hazard, ...). Written for operator review ahead of a
possible restructure.
68ef0f8 made services_converged() require UpdateStatus settled, treating
'paused' as in flight. But swarm's default update-failure-action pauses the
update on a single task flicker and the flag persists FOREVER (until the next
update): immich CI 241 had the app service 'paused' from a restart during
restore while the service was back at 1/1 and healthy — every subsequent wait
hung to its deadline and the run had to be killed.
Only 'updating' and 'rollback_started' now block convergence: those are the
states swarm is actively driving (the 238 stop-first race lives in 'updating').
'paused'/'rollback_paused' make no progress without intervention, so waiting on
them is pointless — N/N replicas is already required, and the HTTP-health and
tier assertions still gate whether the app actually works.
lint: PASS, unit tests: 138 passed.
services_converged() accepted N/N replicas as converged — but a chaos redeploy
that changes a non-app service image (immich PR #2 moves the db to the
vectorchord pin) registers a stop-first rolling update that swarm may not have
STARTED yet: the OLD task still shows 1/1, the wait passes, and the task dies
seconds later. Build 238: backupbot resolved the db hook container, the task
was killed in the gap, and the pre-hook exec crashed the whole backup with a
409 -> no dump in the snapshot -> restore had nothing -> RED.
- services_converged() now also requires every service's swarm UpdateStatus to
be settled ('', completed, rollback_completed) — updating/paused/rollback in
flight is NOT converged. Strictly stricter: no gate is weakened.
- backup_app() gains a bounded (300s) settle-wait before 'abra app backup
create' as defence in depth; on timeout the backup still runs and the tier's
assertion delivers the verdict.
lint: PASS, unit tests: 138 passed.
capacity=2 went live with three stale capacity=1-era assumptions that corrupted
concurrent runs (immich 229/230 '/pg_backup.sh: No such file'):
- ~/.abra/recipes/<recipe> is ONE shared working tree that fetch_recipe rm-rf's/
reclones and the upgrade tier git-checkouts mid-run. Same-recipe runs now
serialise on an exclusive flock (/run/lock/cc-ci-recipe-<recipe>.lock), taken
in main() BEFORE fetch_recipe and held for the whole run; the kernel releases
it on any process death, so there is no stale-lock failure mode. Different
recipes still run in parallel.
- CCCI_JANITOR_MAX_AGE=0 made a starting build reap ANY in-flight run app. Every
run now registers its app domain + pid in /run/cc-ci-active/<domain> before
app creation; the janitor checks the owner: alive (pid is a live run_recipe_ci
process) -> never reaped; dead -> reaped immediately; unknown (pre-registry or
post-reboot) -> age fallback (default 2h). The MAX_AGE=0 env override is gone
from .drone.yml.
- .drone.yml: concurrency.limit 1 -> 2 to match DRONE_RUNNER_CAPACITY=2; the
'safe because capacity=1' comments now describe the flock+registry model.
lint: PASS, unit tests: 138 passed.
Push builds have been RED on the lint step since ~build 209 from accumulated
formatting drift. This is the mechanical cleanup: ruff format + ruff --fix
(UP038 isinstance unions, SIM105 contextlib.suppress, UP031 f-strings, SIM115
tempfile context manager), shfmt -i 2 -ci, nixpkgs-fmt/statix/deadnix (merged
attrsets, dropped unused lib args), yamllint, and shell quoting fixes in
tests/lasuite-docs/setup_custom_tests.sh. No behaviour changes intended;
lint: PASS, unit tests: 138 passed.