# REVIEW — cc-ci Adversary (append-only)

This file is owned by the **Adversary** loop (§6.1). The Builder seeds this stub at bootstrap and
does not edit it afterward. Adversary appends milestone/D-item verdicts (`<id>: PASS @<ts>` +
evidence, or `FAIL` + a finding in `BACKLOG.md ## Adversary findings`), and may write `## VETO`.

<!-- Adversary verdicts below -->

## M0 — Foundations: PASS @2026-05-26T21:35Z

Verified cold (fresh shell, own clone `/srv/cc-ci/cc-ci-adv`, isolated host build dir
`/root/cc-ci-advverify`, no reuse of Builder's `/root/cc-ci`).

Acceptance — "`systemctl is-system-running` healthy after a rebuild from the repo" + Builder's
sops claim:
- **Repo rebuilds cc-ci:** synced M0 commit `deb4a0f` (git-archive, no .git) to host, ran
  `nixos-rebuild build --flake .#cc-ci` → `BUILD EXIT 0`, produced
  `…-nixos-system-nixos-24.11.20250630.50ab793`. Current HEAD also builds clean.
- **System health:** `systemctl is-system-running` → `running`; `systemctl --failed` → 0 units.
- **sops decrypt:** `/run/secrets/test_secret` present, mode `400 root:root`, 41 bytes, value
  begins `cc-c…` (matches claimed generated `cc-ci-m0-…`). `secrets/secrets.yaml` is genuinely
  encrypted (2× `ENC[…]` + sops metadata block).
- **D6 leak probe (early):** the decrypted plaintext value appears **0 times** across *all* git
  history (`git grep -F over git rev-list --all`) and 0× in plaintext in `secrets.yaml`. No leak.

Note (not a finding; context for the M1 gate): the *running* system is already ahead of M0 — its
closure includes docker, `unit-swarm-init`, and **traefik** units (`traefik.yml`,
`traefik-stack.yml`, `unit-traefik-deploy`) that are **not yet committed** (HEAD `ab839ae` is
swarm-only, no traefik). Expected mid-M1 churn, but the Traefik config must be committed to the
repo before M1 is claimed or it fails D8 reproducibility — will check at the M1 gate.

## M1 — Swarm + abra target: PASS @2026-05-26T22:20Z

Verified cold from own clone; deployed my **own** probe recipe via abra (not trusting the Builder's
hand-test). Acceptance "a recipe deployed via abra is reachable over HTTPS at
`*.ci.commoninternet.net`, then fully torn down leaving no volumes" + orchestrator's M1 checklist
(a–d).

- **(a) Real coop-cloud/traefik recipe (not hand-rolled):** `docker service ls` →
  `traefik_…_app` (`traefik:v3.6.15`) + `…_socket-proxy` (lscr.io socket-proxy) — the canonical
  recipe layout, deployed via abra (`scripts/deploy-proxy.sh`). `modules/traefik.nix` is deleted.
- **(b) Wildcard on web-secure + proxy overlay:** static `traefik.yml` has `web-secure: :443`
  (web→web-secure 301 redirect, verified live). File provider `/etc/traefik/file-provider.yml`:
  `tls.certificates: [{certFile:/run/secrets/ssl_cert, keyFile:/run/secrets/ssl_key}]`; swarm
  secrets `…_ssl_cert_v1`/`…_ssl_key_v1` mounted (2909 B / 227 B = the pre-issued cert). My probe
  app `advm1probe_…_app` was attached to the `proxy` overlay.
- **E2E (cold deploy):** `abra app new custom-html -D advm1probe.ci.commoninternet.net` (forced
  `LETS_ENCRYPT_ENV=""`) → `deploy succeeded 🟢`. Via SOCKS proxy: **HTTP 200**; served cert
  `subject: CN=*.ci.commoninternet.net`, SAN-matched, `SSL certificate verify ok`, issuer LE E8 —
  i.e. the **pre-issued wildcard**, NOT a per-host ACME cert.
- **(c) No Gandi/DNS token, no ACME credential:** repo (all history) clean; on host the only
  gandi/dns-challenge strings are **commented-out** recipe-template options (`#GANDI_…`,
  `#SECRET_GANDIV5_…`) holding no value. Active traefik env = `LETS_ENCRYPT_ENV=` (empty),
  `WILDCARDS_ENABLED=1`, `compose.wildcard.yml`. `staging`/`production` certResolvers are *defined*
  in traefik.yml (stock template) but **referenced by no router**; both acme.json are **0 bytes**;
  **0 ACME lines in traefik logs**. No ACME ever fires. (Hardening risk filed — see findings.)
- **(d) Manual renewal documented:** DECISIONS.md — operator re-issues at same paths, then
  `abra app secret rm … ssl_cert` + re-insert at bumped version; install.md "Renewed out-of-band;
  never ACME here."
- **Teardown:** `abra app undeploy` + `volume remove` → post-teardown services/containers/volumes/
  secrets for the probe **all 0**. Also independently confirmed the Builder's `cchtml1` test left 0
  runtime resources (only its inert `.env` config file remains, harmless).

Verdict: **M1 PASS.** Not a hard fail on (c) — no token/credential exists and no ACME fires — but
the inert ACME resolvers + test-app default `LETS_ENCRYPT_ENV=production` are a latent hazard that
goes live when the harness deploys apps; filed as `[adversary]` for M4.

<!-- M2 live-trigger probe @2026-05-26T23:30Z: this push should create Drone build #4 -->

## M2 — Drone online: PASS @2026-05-26T23:32Z

Verified cold from own clone. Acceptance: "push to cc-ci triggers a visible green Drone build."

- **Drone server healthy:** `https://drone.ci.commoninternet.net/healthz` → HTTP 200 via gateway.
  Exec runner (`drone-runner-exec.service`) active, `polling the remote server capacity=2 type=exec`.
- **Repo wired:** in Drone's DB the `recipe-maintainers/cc-ci` repo is `repo_active=1`,
  `repo_config=.drone.yml`. Gitea↔Drone OAuth proven by the in-pipeline `clone` step succeeding
  against the private repo (build can't clone without working OAuth/repo token).
- **Push→green, independently triggered:** I pushed my own commit `91a8e8d` (a REVIEW.md change) →
  Drone created **build #4**, `build_event=push`, `build_trigger=@hook` (Gitea webhook), and it ran
  **`success`**: stage `self-test` exit 0, steps `clone`+`hello` both exit 0. Builds #1–#3 (Builder
  commits) likewise all `success` via `@hook`. (My earlier M0/M1 review pushes predate the
  `.drone.yml`, so correctly produced no builds.)
- **Visible logs (D7 precondition):** `logs` table holds per-step log blobs for every build; Drone
  UI/API serve them. Full D7 UX is M8.

Verdict: **M2 PASS.** No new findings.

## M3 — Comment bridge: PRE-CLAIM PROGRESS (not yet PASS) @2026-05-26T23:48Z

M3 is **Blocked** in STATUS (Gitea not delivering webhooks), so not a gate verdict yet. But the
bridge is deployed and I independently hammered its auth/filter logic — the part I can verify
regardless of the delivery leg (and which survives a pivot to API polling). Probes were live POSTs
to `https://ci.commoninternet.net/hook` via the SOCKS proxy, with HMAC signatures I computed from
the on-host secret (read with root; value never printed/committed):

| probe | expect | got |
|---|---|---|
| no `X-Gitea-Signature` | 401 | **401** |
| bad signature | 401 | **401** |
| valid sig, event=`ping` (not issue_comment) | 204 | **204** |
| valid sig, `!testmexyz` on a real PR | 204 (no trigger) | **204** |
| valid sig, `!testme` but issue is not a PR | 204 | **204** |
| valid sig, `!testme` on PR, action=`edited` | 204 | **204** |
| valid sig, `!testme` on real PR, **non-collaborator** | 403 | **403** |

So: HMAC fail-closed + timing-safe (`compare_digest`, verified before body parse), `!testmexyz`
correctly ignored (exact trimmed match), non-PR ignored, and a non-collaborator is rejected (403;
collaborator status re-checked via Gitea API, not trusted from the signed payload). Source review
of `bridge/bridge.py` found no auth bypass.

**Blocker independently corroborated (operator-side):** the bridge hook *is* registered + active on
`recipe-maintainers/cc-ci` (id 210, events `[issue_comment]` → `ci.commoninternet.net/hook`), and
the bot is not a Gitea site-admin (`GET /admin/hooks` → 403) nor org owner, so it genuinely cannot
inspect/change Gitea's `[webhook] ALLOWED_HOST_LIST`. Endorse STATUS `## Blocked`: needs operator
allowlisting or the documented poll-the-API fallback.

**Still UNVERIFIED for an M3 PASS:** (1) the positive path — a valid collaborator `!testme` actually
starts a build + posts the PR comment end-to-end; (2) real Gitea→bridge delivery (or the polling
pivot). Will complete both when M3 is claimed.

**Noted for M7 (not a finding yet):** the Drone-managed Gitea webhook (id 209) carries its webhook
secret as a `?secret=` query param in the hook URL (Drone default; admin-only in Gitea, not in cc-ci
git / CI logs / dashboard). Will adjudicate against D6 at M7.

## M4 — Harness + install stage: VERIFICATION IN PROGRESS (no verdict yet) @2026-05-27T00:35Z

M4 is CLAIMED. Code review done; runtime checks so far:
- **A1 CLOSED** (see BACKLOG): harness forces `LETS_ENCRYPT_ENV=""` every deploy; live app
  `cust-c95a69` served the wildcard cert, 0 ACME lines, no certresolver.
- **Happy-path teardown works:** a prior run's app `cust-e084bd` was fully torn down (gone) — not
  an orphan; earlier ambiguity was a run cycling apps.
- **Two teardown-robustness defects filed (A2, A3):** janitor's `-pr` filter is dead code under the
  `cust-<hex>` naming (no crash-orphan reaping); teardown is best-effort/unverified and deletes the
  `.env` even on failed undeploy (silent orphan, run still green).
- **Deferred to next idle tick (a Builder harness run is active now; sequential-only):** my own
  cold install run (green install + Playwright + clean teardown verification) and the §6 kill-mid-run
  probe to test A3 empirically. Verdict (PASS/FAIL) follows that.

## M4 — Harness + install stage: PASS @2026-05-27T01:05Z

Verified by my **own** cold harness run (`RECIPE=custom-html REF=advcold… cc-ci-run
runner/run_recipe_ci.py`, app `cust-cfeb6a`, isolated from a Builder run that happened to run
concurrently as `cust-3c1970` — no collision, distinct domains/volumes/secrets):
- **Install stage green:** `test_install.py` → 2 passed (27s): `test_http_reachable` (HTTPS 200 via
  gateway) + `test_playwright_page` (real Chromium loads the live app, status 200, served HTML).
- **Guaranteed teardown:** after the run, `cust-cfeb6a` left **0** services / volumes / secrets /
  containers / `.env` — fully clean. Infra (traefik/drone/bridge/backups) untouched.
- A1 closed (no-ACME enforced). **Open robustness findings A2 (dead `-pr` janitor) + A3 (unverified
  best-effort teardown)** concern the *crash* path (finalizer-skipped), not this happy-path run;
  they don't block M4's literal acceptance but must be resolved before DONE (D2 teardown guarantee).
  Kill-mid-run probe to substantiate A2/A3 deferred until the host is idle.

Verdict: **M4 PASS.**

## M5 — Upgrade + backup/restore stages: PASS @2026-05-27T01:05Z

Same cold run, stages 2 and 3 — both genuine end-to-end (no mocks; assertions reviewed in source
and not softened):
- **Upgrade green:** `test_upgrade.py` → 1 passed (41s). Deploys the **previous published version**
  (`previous_version` = `recipe_versions[-2]`), writes a marker into the volume-backed html dir,
  upgrades to latest (`abra upgrade`), then asserts HTTP 200 **and** the marker survives — a real
  version change with data persistence across the volume (`cust-…_content`), not a no-op.
- **Backup/restore green:** `test_backup.py` → 1 passed (37s). Writes `original`, `abra backup`,
  mutates to `mutated` (asserted), `abra restore`, then asserts the served content is back to
  `original` ("restore did not return the pre-mutation state"). Real backup→mutate→restore cycle
  via backup-bot-two.
- Teardown clean (same `cust-cfeb6a` 0-remnant check above covers all three stages — same domain
  reused per stage).

Verdict: **M5 PASS.**

## M6 — Recipe-local tests + second recipe: VERIFICATION IN PROGRESS (no verdict yet) @2026-05-27T01:48Z

M6 CLAIMED. Host has been continuously busy (Builder M6.5 ramp), so deploy-based checks are
deferred to an idle window; static + evidence review so far:
- **custom-html 3-stage:** already verified cold by me (see M5 PASS) — green + clean teardown.
- **D4 recipe-local discovery — code genuine:** `run_recipe_ci.snapshot_recipe_tests` copies the
  recipe-shipped `tests/` before abra re-checkouts to a version tag, then `run_recipe_local` deploys
  the app and runs those tests against the LIVE app via `CCCI_BASE_URL`/`CCCI_APP_DOMAIN`, merged as
  a separate stage with guaranteed teardown. Demo branch `recipe-maintainers/custom-html@
  ci/d4-recipe-local` confirmed to ship `tests/test_recipe_local.py` (Gitea API). Will run it cold to
  confirm the stage executes+passes.
- **keycloak (#2) install — test genuine:** `/realms/master` 200 health + real Playwright admin
  console login (waits for the username field). `recipe_meta.py` (HEALTH_PATH/timeouts) confirms D5
  "no harness surgery". Empirical keycloak reproduction deferred (heavy deploy; idle window).
- **Filed [adversary] A4** (concurrency): same-recipe concurrent runs share `~/.abra/recipes/<recipe>`
  with no isolation/lock/concurrency-cap — a collision vector for the §6 concurrency check; to
  confirm empirically.

Pending for idle host: cold D4 run, keycloak reproduce, A2/A3 kill-probe re-test, A4 concurrency test.

## D6/M7 — preliminary leak scan of published Drone logs (PASS so far; M7 not yet claimed) @2026-05-27T02:05Z

Host-safe probe while the host was busy. Pulled Drone's `database.sqlite`, dumped all 42 `logs`
rows (~25.5k chars of published per-step build output), scanned:
- **Known infra secrets — 0 leaks:** webhook HMAC (64), drone token (32), gitea token (40) each
  appear **0×** in the logs (exact `grep -F`).
- **No value patterns:** 0 matches for `password|secret|token = <value>`.
- The only long hex/base64 hits are **git commit SHAs** in `git clone/merge` output — benign.
Caveat: current Drone logs are hello-world + self-test; the full M7/D6 test must also cover
app-generated secrets (e.g. keycloak DB passwords) in recipe-run logs AND the dashboard (M8). This
is a clean baseline, not the final D6 verdict. (DB copy was scanned off-box and deleted; no secret
value printed or committed.)

## M3 — Comment bridge: PASS @2026-05-27T03:13Z

Verified cold against the NEW design (orchestrator change: polling-PRIMARY + org-membership auth;
webhook now optional). Re-reviewed `bridge/bridge.py` (256 lines) — sound — then live-probed the
running bridge + Drone:
- **`!testme` triggers a run ≤60s:** I posted `!testme` (comment 13708) on PR #1 at epoch
  1779847690 → bridge `[poll] triggered build 35` → Drone build 35 created at 1779847702 =
  **12s** latency. (Build is `failure` only because `RECIPE=cc-ci` has no `tests/cc-ci/`; the
  trigger + event=custom recipe-CI pipeline fired correctly — integration is live.)
- **Re-commenting re-runs:** my new comment 13708 → build 35, distinct from the earlier
  comment 13705 → build 26. Distinct comment ids each fire once (dedup via `_claim`).
- **Other comments do NOT trigger:** I posted `!testmexyz` → **no** build created, no bridge
  trigger log. Exact trimmed match enforced.
- **Auth enforced (org-membership, fail-closed):** `GET /orgs/recipe-maintainers/members/<u>` —
  autonomic-bot & notplants → 204 (allowed), `definitely-not-a-member-zzz9` → 404 (rejected).
  `is_authorized` returns True only on 204/allowlist; anything else (incl. errors) → False.
- **Link back:** bridge posted run-link comment 13706 ("cc-ci: started CI run … → drone…/recip…").
- **Concurrency cap live:** runner `capacity=1` (`DRONE_RUNNER_CAPACITY=1`) + pipeline
  `concurrency:limit:1` — recipe-CI builds serialize.

Verdict: **M3 PASS.** (Polling is outbound read+comment only — no repo-admin; webhook optional.)
Note: full bridge→3-stage-recipe-CI E2E on a *real recipe* PR is the Builder's in-flight
integration item / D10 — build 35 shows the pipeline wiring works; green-on-a-real-recipe is M10.

## D6 — leak scan extended to recipe-CI build logs (still clean) @2026-05-27T04:05Z

Followup to the earlier hello-world scan: scanned the logs of all 7 `event=custom` recipe-CI builds
(~26.7k chars — these ran real `abra app deploy` + `abra app secret generate`, so generated app
secrets *could* surface here). Result: **0** `password|secret = <value>` patterns, **0** "secret
generated/inserted" value lines (abra doesn't echo secret values), and every long hex/base64 hit is
benign — Nix store paths, git SHAs, Drone workspace dir names (`<rand16>/drone/src`), pytest
tracebacks. No app-secret leak in published recipe-run logs. (Full M7/D6 verdict still pending the
dashboard (M8) leak check + final M7 claim.)

## M6 — Recipe-local tests + second recipe: PASS @2026-05-27T04:43Z

Acceptance: "both recipes green (custom-html 3-stage; keycloak install) + recipe-local merged",
plus D4/D5. Verified by a mix of my own cold runs + deep Drone-log corroboration (keycloak's 31-min
deploy made a self-rerun impractical on the contended host, so I read the actual build #39 logs, not
a Builder summary):
- **custom-html 3-stage:** my own cold run (see M5 PASS) — install/upgrade/backup green, 0 orphans.
- **keycloak (#2) full 3-stage — build #39 (event=custom, RECIPE=keycloak, success):** actual log
  lines show `PASSED test_realm_endpoint_healthy`, `PASSED test_playwright_admin_login` (install,
  510s), `PASSED test_upgrade_preserves_realm` (upgrade, 610s — DB realm survived), `PASSED
  test_backup_mutate_restore` (backup, 495s — realm restored). Three separate reported stages (D2).
  Tests are genuine (admin REST + real Playwright admin-console login; reviewed source — not mocked).
  Post-run: **0** keycloak services/volumes (clean teardown).
- **D4 recipe-local — verified by my OWN run:** `RECIPE=custom-html SRC=…/custom-html
  REF=ci/d4-recipe-local` → recipe-shipped `tests/test_recipe_local.py` snapshotted to a temp dir
  (immune to abra's version re-checkout), deployed the app, ran
  `test_recipe_local_serves_content PASSED` against the LIVE app via `CCCI_BASE_URL`, merged as a
  `recipe-local` stage; clean teardown (0 `cust-` leftovers).
- **D5 (no harness surgery):** keycloak enrolled via `tests/keycloak/` + `recipe_meta.py` only; no
  changes to shared `runner/harness` code. enroll-recipe.md documents the flow.

Verdict: **M6 PASS.** (keycloak full 3-stage also satisfies the first M6.5 breadth slot.)

## M6.5 — breadth ramp: RUNNING EVIDENCE (no verdict yet — recipes 5–6 + gate pending) @2026-05-27T06:12Z

Deep-corroborating each recipe's canonical Drone recipe-ci build from its actual logs (genuine
3-stage assertions, not summaries). Confirmed green so far (categories in parens):
- **custom-html** (simple/stateless) — build #33 + my own cold 3-stage run (M4/M5).
- **keycloak** (SSO + DB-backed) — build #39: realm health + Playwright admin login (install),
  `test_upgrade_preserves_realm`, `test_backup_mutate_restore` (M6 verdict).
- **cryptpad** (stateful, no external DB) — build #46: `test_http_reachable`,
  `test_playwright_loads_cryptpad`, `test_upgrade_preserves_data`, `test_backup_mutate_restore`.
- **matrix-synapse** (large-volume / DB + media store) — build #51: `test_client_api_healthy`,
  `test_client_api_advertises_versions`, `test_upgrade_preserves_data`, `test_backup_mutate_restore`.
All three stages reported separately per build (D2). Categories covered: simple, SSO/DB, stateful,
large-volume. **Remaining:** recipe #5/#6 (multi-service+S3/object-storage, e.g. lasuite; and the
6th for breadth) + the M6.5 gate. Final M6.5/D10 verdict after those + the §6 concurrency check.

## Reconciliation @2026-05-27T06:18Z (watchdog ping)

Checked all standing claims: **every CLAIMED milestone gate through M6 is Adversary-PASS** —
M0 @21:35, M1 @22:20, M2 @23:32, M3 @03:13, M4 @01:05, M5 @01:05, M6 @04:43 (all <24h). The
"Gate: M0/M1/M2/M3 — CLAIMED, awaiting Adversary" strings still present in STATUS.md §Gates are
**stale** (already cleared here); a watchdog scanning that section may false-positive on them —
Builder may want to annotate them PASS. **No open milestone claim right now:** M6.5 is in-flight
(4/6 recipes corroborated green: custom-html/keycloak/cryptpad/matrix-synapse; recipes 5–6 + the
M6.5 gate pending), M7/M8/M9/M10 not yet claimed. Open findings: A2 (live janitor sweep pending an
idle host; mechanism already verified). Nothing for me to verify is currently blocked on me.

## M6.5 — Breadth ramp (recipes 3–6): PASS @2026-05-27T07:25Z

Acceptance: "recipes 3–6 each full three-stage green; enrolling N≥3 needed no shared-harness changes."
All six recipes' canonical Drone recipe-ci builds deep-corroborated from their actual logs (genuine
assertions + 3 separately-reported stages each; clean teardown):
- **cryptpad** #46 (stateful) — http + Playwright, `test_upgrade_preserves_data`, `test_backup_mutate_restore`.
- **matrix-synapse** #51 (large-volume/DB+media) — `test_client_api_healthy`/`_advertises_versions`,
  `test_upgrade_preserves_data`, `test_backup_mutate_restore`.
- **lasuite-docs** #57 (multi-service + S3/MinIO) — `test_http_reachable`, `test_playwright_loads_frontend`,
  `test_upgrade_preserves_data`, `test_backup_mutate_restore`.
- **n8n** #63 (workflow) — `test_healthz`, `test_playwright_loads_editor`, `test_upgrade_preserves_data`,
  `test_backup_mutate_restore`.
  (recipes 1–2 custom-html #33/keycloak #39 verified under M4/M5/M6.)
- **D5 (no harness surgery) verified:** grepped shared harness (`runner/harness`, `conftest`,
  `run_recipe_ci`) — **no per-recipe branching** (`if recipe==…`); the only recipe names there are
  comments. Per-recipe quirks (cryptpad SANDBOX_DOMAIN, health paths, timeouts) live in
  `tests/<recipe>/recipe_meta.py` and are consumed via the generic `EXTRA_ENV`/meta hook in
  `deploy_app`. Enrolling a recipe = `tests/<recipe>/` + `recipe_meta.py` only.
- **bluesky→n8n swap is plan-sanctioned + documented** (DECISIONS): bluesky-pds needs TLS-passthrough
  to an in-container caddy doing its own ACME — incompatible with the no-DNS-token/no-ACME design;
  documented non-CI'd recipe (per §2's explicit allowance). The 5 required D10 categories
  (simple/SSO+DB/stateful/large-volume/multi-service+S3) are covered without it.

Verdict: **M6.5 PASS.** Note: these builds were triggered as recipe-ci custom builds (RECIPE param);
the **real `!testme`-on-a-PR** end-to-end for the breadth set is D10/M10, still to verify.

## M7 — Secrets hardening (D6): PASS @2026-05-27T07:55Z

Acceptance: "Adversary's secret-grep over published logs finds nothing; rotation doc followed."
Verified the §9 hard rule (no plaintext secret in git, logs, or UI) across ALL surfaces:
- **Published Drone logs — clean:** dumped every `logs` row across all builds (~119k chars; incl. the
  6 recipe runs that generate app secrets). The 3 infra secrets (webhook HMAC / drone token / gitea
  token, read from `/run/secrets`) each appear **0×**; no `password|secret|token=<value>` patterns;
  long-token hits are git SHAs / nix paths / Drone workspace names (benign).
- **Dashboard — clean:** `https://ci.commoninternet.net/` (200) + `/badge/*.svg`: 0 secret patterns,
  0 infra-secret values.
- **Git (all history) — clean:** each infra secret **0×**; `secrets/secrets.yaml` is sops-encrypted
  (7× `ENC[…]`). No plaintext infra secret committed.
- **Redaction filter** (`run_recipe_ci.run_stage_redacted`): masks any `/run/secrets/*` value (≥8
  chars) in stage stdout before it reaches Drone. Present as a safety net; 0 `REDACTED` markers in
  logs = no secret was ever echoed in the first place.
- **Rotation doc (`docs/secrets.md`) matches reality:** `.sops.yaml` has exactly the documented two
  recipients — host key `age1h90ut…` (from cc-ci's ed25519 SSH host key) + off-box master recovery
  `age1cmk26t…`; sops-nix decrypts to `/run/secrets/<name>` (0400 root) using the SSH host key
  (verified at M0 + present now). A1/A2 split + rotation steps are coherent.

Minor (not a finding): the redaction list covers infra secrets only, not per-run generated app
secrets — but abra doesn't echo generated secrets (recipe logs clean) so no app-secret ever surfaced.

Verdict: **M7 PASS.**

## M8 — Dashboard (D7): PASS @2026-05-27T08:10Z

Acceptance: "overview matches reality across several runs; outcomes mirrored to PR comments."
- **Overview matches reality:** `https://ci.commoninternet.net/` lists all 6 enrolled recipes, each
  `success` with the **exact canonical build #s I independently corroborated** (cryptpad #46,
  custom-html #33, keycloak #39, lasuite-docs #57, matrix-synapse #51, n8n #63) + relative "last run"
  times; cc-ci itself correctly excluded; 30s auto-refresh; YunoHost-CI-like recipe table + status
  badges, dark theme.
- **Status badges:** `/badge/keycloak.svg` encodes `success` (per-recipe embeddable badge).
- **PR-comment outcome reflection:** on PR #1 the bridge posted a start comment (id 13709 → run #35)
  and a **final-outcome** comment (id 13712: "run for `cc-ci` @ `d397720a` ❌ **failure** → …/76") —
  mirrors the final pass/fail and links the run. (Failure case shown; success path is the same code.)
- **No secret leak** on the dashboard/badges (verified under M7).

Verdict: **M8 PASS.** (A green ✅ outcome reflected on a *real recipe* PR is exercised at D10/M10.)

## M10/D10 — independent confirmation of the Docker Hub rate-limit blocker @2026-05-27T10:25Z

The Builder filed lasuite-docs upgrade failing on Docker Hub anonymous pull rate limits (A1 registry
creds needed; 5/6 recipes green via real `!testme`). I disbelieved and verified — it is **real, not a
masked harness defect**:
- Queried Docker Hub's rate-limit headers from cc-ci's own source IP (68.14.43.142):
  `ratelimit-limit: 100;w=21600`, **`ratelimit-remaining: 1`** — i.e. ~1 anonymous pull left in the
  6h window. The D10 breadth runs (6 recipes, lasuite alone = 9 images) drained the anonymous quota.
- lasuite Drone builds (#88/#92 failure, #93 killed) show no `toomanyrequests` in pytest output —
  expected, because a rate-limited pull manifests at the docker/swarm task layer (deploy/health
  timeout), not in the test log; the header check is the direct proof.
- The CI system itself is sound: lasuite install + backup are green; only the upgrade stage (most
  image pulls) is gated, and only by the external quota. This is precisely the plan's anticipated A1
  input (§1.5/§4.4: "rate-limit failure traced to this is a finding, then request creds").

**Consequence for DONE:** D10 requires all 6 recipes green via real `!testme` with all 3 stages.
lasuite-docs upgrade cannot reliably pass without authenticated registry pulls. **This is an
operator-action blocker** (provide Docker Hub creds → sops `secrets/`), analogous to the M3 webhook
whitelist. Not a VETO of system quality; a missing external input. DONE must wait until lasuite's
upgrade goes green via `!testme` (creds provided, or quota-window retry verified stable).

## M10/D10 — real-!testme proof: 5/6 VERIFIED (6th blocked on registry creds) @2026-05-27T10:42Z

Independently verified the full real-`!testme` path (D1 trigger + D2 three genuine stages + D7
outcome reflection) for 5 of 6 recipes, from a cold read of Drone + bridge logs + Gitea PR comments:
| recipe | build | bridge poll-trigger (real !testme) | stages | result |
|---|---|---|---|---|
| custom-html | #84 | PR#2 comment 13717 | 3 (4 asserts) | success |
| keycloak | #86 | PR#1 comment 13719 | 3 (4 asserts) | success |
| matrix-synapse | #87 | PR#1 comment 13720 | 3 (4 asserts) | success |
| n8n | #89 | PR#1 comment 13722 | 3 (4 asserts) | success |
| cryptpad | #90 | PR#2 comment 13727 | 3 (4 asserts) | success |
- Each build is `event=custom` with `REF`=PR-head sha (tests the PR's code, D1), 3 separately-reported
  stages install/upgrade/backup (D2), and the bridge logged a genuine `[poll] triggered build N …
  by autonomic-bot` for each (real comment, not a manual build).
- **Outcome reflection (D7):** verified on keycloak PR#1 — `!testme` → bridge comment "run for
  `keycloak` @ 04400dff ✅ **passed** → …" (success path; ❌ failure path seen earlier on cc-ci).
- **6th recipe lasuite-docs:** install+backup green via `!testme`, **upgrade blocked** on the
  Docker Hub anon rate limit (independently confirmed: remaining 1/100). Category = multi-service +
  S3/object-storage; until its upgrade is green via `!testme`, **D10 is not fully met** (5/6).

Verdict: **D10 PARTIAL (5/6)** — pass for 5; the 6th awaits operator registry creds. No system defect;
the gap is the external pull quota. DONE must wait for lasuite's 3rd stage green via `!testme`.

## M9/D8 — Reproducibility: core PROVEN; full live blank-VM rebuild pending registry creds @2026-05-27T10:52Z

D8 ("entire server declared in the flake; rebuildable from scratch per docs/install.md; Adversary
rebuilds on a throwaway VM OR documents why infeasible + what was tested"). Done so far:
- **Nix-level reproducibility PROVEN (strongest evidence the repo *is* the server):** synced repo
  **HEAD** (clean `git archive`, no .git) to an isolated host dir, ran `nixos-rebuild build
  --flake .#cc-ci` → `BUILD EXIT 0`, and the built closure
  `…m1pdvbhlmlj3x3gn0x83rgwcgssks7qs-nixos-system…` is **byte-identical to `/run/current-system`**.
  So the entire running server (swarm, drone, traefik reconcile, comment-bridge, dashboard,
  backupbot, sops secrets) is fully declared in the repo with **zero uncommitted drift** — a clean
  rebuild reproduces it exactly. (`nixos-rebuild build` is not rate-limited; image pulls happen at
  swarm runtime.)
- **docs/install.md is a complete from-scratch path:** operator preconditions (A1) + the whole
  install = clone + one `nixos-rebuild switch` (reconcile oneshots auto-converge proxy/drone/bridge/
  dashboard) + one-time `bootstrap-drone-oauth.sh`. Accurate vs. the verified architecture.
- **Deferred (per plan's documented-alternative allowance):** a full from-scratch LIVE deploy on a
  blank NixOS VM (incus available) pulls every recipe/infra image at swarm runtime → hits the **same
  Docker Hub anon rate limit** confirmed under M10 (remaining 1/100). Since DONE is already gated on
  those operator registry creds, I will do the throwaway-VM live rebuild **when creds arrive**
  (unblocks D8 live + D10 lasuite together) rather than wall against the quota now.

Status: **D8 reproducibility core PASS (Nix + docs); live blank-VM rebuild pending creds** — to
complete before DONE.

## D9 — Documentation: PASS @2026-05-27T10:55Z

Acceptance: "README + docs/ explain architecture, enroll a recipe, add/run tests locally, operate/
rotate secrets, debug a failed run; a new engineer can enroll a recipe and get a green run using
only the docs." Reviewed the full set:
- **architecture.md** — components, the `!testme` flow, network/TLS, resource safety.
- **enroll-recipe.md** — mirror the recipe → add `tests/<recipe>/` tree → recipe-local (D4) → add to
  bridge poll list → optional webhook → run locally. Matches the verified enroll mechanism (D5: I
  confirmed enrolling needs only `tests/<recipe>/`+`recipe_meta.py`, no harness surgery).
- **runbook.md** — where to look, common failure modes, orphans/cleanup, re-run/trigger by hand,
  cancel a stuck build (debug a failed run).
- **secrets.md** — sops model + rotation (verified accurate vs reality under M7).
- **install.md** — from-scratch server build (verified reproducible under M9/D8).
- **README** — entrypoint, `!testme` overview, repo layout.
The enroll flow documented matches what I exercised hands-on for D4/M6 (custom-html recipe-local) and
what the Builder used for recipes 2–6 with no harness changes. Coverage is complete & accurate.

Verdict: **D9 PASS.**

## Scrutiny — lasuite `abra app upgrade -c` (no-converge-checks) is NOT a test-softening @2026-05-27T11:45Z

The Builder's fix (575efb5) for lasuite's upgrade "convergence failure" adds `-c` to `abra app
upgrade`. Per the anti-drift rule I checked whether this weakens the test to make a red pass — it
does **not**:
- `-c` disables only **abra's** convergence poll, which false-fails a slow 9-service rolling upgrade
  (stop-first roll while pulling new images) even when services do converge.
- The harness's own verification post-upgrade is fully intact and is the real gate:
  `test_upgrade_preserves_data` → `upgrade_app` → **`wait_healthy`** (= `services_converged`: every
  stack service N/N replicas, looped up to recipe_meta `DEPLOY_TIMEOUT`=900s + HTTP health loop),
  then asserts `http_get ∈ {200,301,302}` **and** a real `psql` read that the pre-upgrade
  `ci_marker` row survived ("postgres data did not survive the upgrade").
- So a genuinely failed upgrade (services never reach N/N, app unhealthy, or DB data lost) **still
  fails** the stage. The change trades abra's buggy/impatient check for the harness's more patient +
  more meaningful one.
Cleared as legitimate. **Still required for D10 6/6:** an empirical lasuite upgrade **green via real
`!testme`**, whose build log I'll confirm shows genuine convergence (N/N) + the data-survival
assertion passing — not just absence of an abra error.

## M10/D10 — Proof: 6/6 PASS @2026-05-27T11:57Z

All six recipes now green via REAL `!testme` PRs, all three stages genuinely exercised — the 6th
(lasuite-docs) corroborated this tick:
- **lasuite-docs build #108** (event=custom, REF=9f685240=PR#1 head): real trigger confirmed in
  bridge log (`[poll] triggered build 108 for lasuite-docs@9f685240 (PR #1, comment 13738) by
  autonomic-bot`). 3 stages green: install (`test_http_reachable`, `test_playwright_loads_frontend`,
  148s); **upgrade `test_upgrade_preserves_data` PASSED (141s)** — with the `-c` fix, the harness's
  own `wait_healthy` (9 services N/N) + the `psql` data-survival check passed (no "did not survive"),
  so the upgrade genuinely converged + DB data persisted (NOT hollowed by `-c`); backup
  `test_backup_mutate_restore` PASSED (158s).
- Full D10 set (all via real `!testme`, comment-reflected): custom-html #84 (simple), keycloak #86
  (SSO/identity+DB), matrix-synapse #87 (large-volume/DB+media), n8n #89 (workflow), cryptpad #90
  (stateful), lasuite-docs #108 (multi-service+S3/object-storage). All 5 required categories covered.
- Registry creds (A1) turned out NOT to be required — the real blocker was abra's false-convergence
  check (fixed by `-c`); the rate limit was transient (quota recovered). Creds remain a documented
  good-to-have for robustness.

Verdict: **D10 PASS (6/6).**

## D8 — Reproducible server: PASS (documented-alternative) @2026-05-27T12:00Z

D8 accepts either a throwaway-VM rebuild OR "documenting why a full from-scratch rebuild was
infeasible and what was tested instead." A full from-scratch **live** rebuild on a throwaway host is
**infeasible by design**, for two immovable reasons I verified:
1. **sops is bound to cc-ci's host identity** — `modules/secrets.nix` decrypts via
   `/etc/ssh/ssh_host_ed25519_key`; `.sops.yaml` recipients are only cc-ci's host age key + the
   master recovery key. A throwaway VM (different host key) is not a recipient → cannot decrypt the
   infra secrets → drone/bridge/etc. can't start without operator re-keying.
2. **Operator preconditions are cc-ci-specific** — the pre-issued wildcard cert
   (`/var/lib/ci-certs/live`) and the DNS `*.ci.commoninternet.net → gateway → (passthrough) cc-ci`
   point at cc-ci itself; they can't be reproduced on a throwaway VM (operator-owned, immovable).
**What was tested instead (stronger than a fresh-VM rebuild):** synced repo HEAD (clean, no .git) to
an isolated dir and `nixos-rebuild build --flake .#cc-ci` produced a closure **byte-identical to
`/run/current-system`** — i.e. the entire running server (swarm, drone, traefik reconcile,
comment-bridge, dashboard, backupbot, sops) is fully declared in the repo with **zero uncommitted
drift**; a clean rebuild reproduces it exactly. install.md is an accurate single-`nixos-rebuild`
from-scratch path + the documented operator preconditions. Every component was independently verified
live on cc-ci (M0–M10).

Verdict: **D8 PASS** (Nix reproducibility proven byte-for-byte; throwaway-VM live rebuild infeasible
by design — documented per the plan's explicit allowance).

## DONE-readiness (Adversary) @2026-05-27T12:00Z

All D1–D10 have an Adversary PASS dated within 24h, and findings A1–A4 are all closed. **No VETO.**
| D | verdict | when |
|---|---|---|
| D1 trigger | PASS | M3 03:13 + D10 real-!testme runs |
| D2 3-stage matrix | PASS | M4/M5/M6 + D10 6/6 (real, 3 stages each) |
| D3 Playwright | PASS | live in every recipe install/D10 run |
| D4 recipe-local | PASS | M6 (own run) |
| D5 per-recipe tree / no harness surgery | PASS | M6.5 |
| D6 secrets | PASS | M7 (grep clean: logs+dashboard+git) |
| D7 results UX | PASS | M8 (overview matches reality + PR outcome) |
| D8 reproducible server | PASS | byte-identical build==running + documented-alt |
| D9 docs | PASS | full docs set reviewed |
| D10 six recipes via !testme | PASS (6/6) | #84/#86/#87/#89/#90/#108 |
From the Adversary side, the DONE handshake (§6.1) is **CLEARED** — Builder may flip STATUS → DONE.
(Note: registry creds remain a documented good-to-have for rate-limit robustness, not a DONE blocker.)

## Adversary sign-off on DONE @2026-05-27T12:12Z

STATUS shows `## DONE` (Builder, 1c10fa5). Final cold reality check confirms it is not a ledger lie:
- All D1–D10 carry an Adversary PASS dated 2026-05-27 (<24h); findings A1–A4 all **closed**; **no
  standing `## VETO`**.
- Live system: `systemctl is-system-running` → running, 0 failed units.
- Dashboard (`ci.commoninternet.net`): **6/6 recipes success**, matching the corroborated Drone
  builds (#84/#86/#87/#89/#90/#108, all real-`!testme`, 3 genuine stages each).
- Steady state clean: **0** orphaned `<tag>-<6hex>` test apps/volumes; teardown + janitor verified.
The DONE is **confirmed**. Adversary loop terminating — exit condition met (STATUS `## DONE` + fresh
PASS logged for every D1–D10). Standing note: Docker Hub registry creds remain a documented
good-to-have for rate-limit robustness (not a correctness gap).

---
## SUPERSEDED by Phase 1c (appended @2026-05-27 18:55Z)
The Phase-1 D8 verdict above (and the "throwaway-VM live rebuild **infeasible by design**" wording
at lines ~485–502) is **CORRECTED / superseded** by Phase 1c. The premise no longer holds: the
project's own recovery age key decrypts the repo's secrets on a fresh host, and the wildcard cert is
now sops-in-git — so a from-scratch live rebuild IS feasible and has been **performed and verified**.
Adversary cold-proved it 2026-05-27: a blank NixOS Incus VM + the two git repos + the single
bootstrap age key → one `nixos-rebuild switch` → fully-converged cc-ci, byte-identical (`ld19aj2`),
0 failed, 6 stacks 1/1, cert decrypted from git, TLS leaf == git cert. See REVIEW-1c.md (W4/C4/C5
PASS). D8 is now honest: static byte-identical **plus** live throwaway rebuild; "infeasible by design"
is withdrawn.
