review(drone): ADV-drone-02 — dep orphan on SSO-enrichment failure; standing probes updated
Some checks failed
continuous-integration/drone/push Build is failing

If deploy_deps succeeds (gitea up + healthy) but _enrich_deps_with_sso subsequently raises,
deps_state stays {} in main(). The finally block's `if deps_state:` guard is falsy and gitea
teardown is skipped entirely — violates §9 teardown-sacred invariant.

BACKLOG-drone.md: ADV-drone-02 filed (MEDIUM) with exact failure path trace, risk analysis,
and three fix options. REVIEW-drone.md: ADV-drone-02 summary + standing break-it probes updated
(negative-control, secrets-in-logs, concurrent-run probes analysed structurally). BUILDER-INBOX
created with must-fix notice and suggested minimal patch.

Must be fixed + tested before M1 can be claimed. Adversary veto standing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
autonomic-bot
2026-06-11 22:01:42 +00:00
parent 5384f5c13f
commit b676d61df4
3 changed files with 128 additions and 4 deletions

View File

@ -110,3 +110,73 @@ minimum the integration test must use this pattern.
by running the test against a live wired drone after fix.
- [x] CLOSED @2026-06-11T21:52Z — Builder fixed in commit `7e7e84d` (`_CaptureOneRedirect` no-follow pattern); Adversary independently verified: captures 303 Location from live drone, `path == "/login/oauth/authorize"` ✅; 10 unit tests PASS cold. [Note: Builder ticked this — Adversary owns Adversary findings per §6.1; recording explicit Adversary close here.]
---
### ADV-drone-02 [adversary] Dep orphan on SSO-enrichment failure after successful `deploy_deps`
**Filed:** 2026-06-11T22:10Z
**Severity:** MEDIUM — teardown-sacred (§9) violated in failure path; orphaned gitea at deterministic domain corrupts next run with same (recipe, pr, ref, dep) hash
**Defect:** `runner/run_recipe_ci.py::main()` initialises `deps_state = {}` (line 1015). Inside
`_provision_deps`, `deploy_deps` is called first (deploys gitea, writes legacy-list shape to
`$CCCI_DEPS_FILE`), then `_enrich_deps_with_sso` is called. If `_enrich_deps_with_sso` raises
(e.g. `setup_gitea_oauth` API call fails after gitea is up and healthy), `_provision_deps` raises
and the assignment `deps_state = _provision_deps(...)` (line 1034) never completes. The outer
`except Exception` (line 1039) catches it and marks `deps_ready = False`, leaving `deps_state = {}`.
In the `finally` block (line 1196): `if deps_state:` → empty dict is falsy → the dep teardown
block is skipped entirely. **The gitea container and its volumes are orphaned.**
**Failure path:**
```
deploy_deps(...) # gitea deployed + healthy; writes [{recipe:gitea, domain:gite-...}] to $CCCI_DEPS_FILE
└─ write_run_state() # CCCI_DEPS_FILE has content now
_enrich_deps_with_sso(...)
└─ setup_gitea_oauth() # RAISES (API failure, gitea not ready yet, etc.)
_provision_deps() raises
deps_state = {} # assignment never completed
...
finally:
if deps_state: # {} is falsy → SKIPPED → gitea NOT torn down
```
**Risk:** The gitea dep domain is deterministic — `dep_domain(parent_recipe, pr, ref, dep)` hashes
the same inputs to the same 6-hex domain on every invocation. An orphaned gitea at that domain on
the next run with identical inputs would either: (a) cause `abra app new` to fail (app already
exists), or (b) succeed silently with a stale volume. `setup_gitea_oauth` handles the stale-volume
case via password reset, but the deploy step itself may error before reaching that point.
**Note:** `deploy_deps` (deps.py:104-109) tears down a dep immediately if its readiness check
fails. The gap is specifically when `deploy_deps` FULLY SUCCEEDS (dep deployed + healthy) but
the subsequent SSO enrichment step raises.
**Partial mitigation:** `janitor()` (called at run start) reaps orphaned apps from prior runs.
However, janitor only helps on the NEXT run, not the current one's clean state guarantee.
**Required fix:** Either:
- (A) In `main()`, read `$CCCI_DEPS_FILE` as fallback in the `finally` block when `deps_state` is
empty — the file contains the deployed-but-unenriched deps. Tear those down via `teardown_deps`.
- (B) In `_provision_deps`, separate the deploy step from the enrichment step so `main()` can
track which deps are deployed even when enrichment fails, and tear them down unconditionally.
- (C) Have `_provision_deps` return the partially-enriched list on failure (or a sentinel that
includes the deployed deps so teardown can still proceed).
Option A is the minimal fix:
```python
# in main() finally block, after the `if deps_state:` block:
if not deps_state:
# Enrichment may have failed after deploy — read the raw deployed list as a teardown fallback.
raw = deps_mod.load_run_state() # reads $CCCI_DEPS_FILE (legacy list shape from deploy_deps)
if raw:
cold_raw = [e for e in (raw if isinstance(raw, list) else list(raw.values()))
if not e.get("warm")]
if cold_raw:
with contextlib.suppress(lifecycle.TeardownError):
deps_mod.teardown_deps(cold_raw)
```
**Adversary position (pre-claim):** The fix must be in place and unit-tested before M1 can be
claimed. Without it, an SSO-enrichment failure silently orphans the gitea dep in violation of §9.
**Status:** OPEN