feat(repair): governed promotion lifecycle with campaigns, canary, and rollback (Section 9) - #265
Merged
Merged
Conversation
…d rollback (Section 9) Replace immediate repair promotion with an explicit, auditable lifecycle: candidate -> reviewed -> replay campaign -> fault campaign -> approved -> staged -> canary -> active. - RepairCandidate artifact (openadapt_flow/repair/candidate.py): changed binding as per-step field diffs (values digested, never raw), failure evidence refs (path + hash), privacy-safe failure fingerprints, campaign results, approval, canary metrics, full transition history. Never auto-activates. - Campaign gates (repair/campaign.py): replay campaign reuses the existing deterministic drift battery (runtime/healing/perturbation.py) over healthy cases; fault campaign adds an adversarial battery (ambiguity, wrong entity, stale target, unexpected dialog, verifier failure) where every case must be REFUSED. Results recorded on the candidate. - Invariant enforcement (repair/invariants.py): a candidate may never weaken identity, effect, risk, environment, or policy requirements; any weakening hard-refuses (fail closed) unless the proposed bundle carries an explicit new qualification revision. - Human approval + atomic activation (repair/lifecycle.py): approval requires a human identity (CLI: openadapt-flow repair approve, with --non-interactive requiring an explicit --approved-by); activation is an atomic pointer swap by exact whole-bundle content digest with lineage and one-command rollback (openadapt-flow repair rollback). - Canary: bounded first-N runs with per-run verification; any silent-incorrect or verification regression automatically reverts the pointer to the prior bundle and drops the candidate back to staged. - Sync + fingerprints: approved/staged/active states are identified by exact content digests; candidate metadata is privacy-safe by construction. - Hard rule: a model actor can never perform any lifecycle transition (assert_not_model_actor); model suggestions flow through the same human-gated path. Asserted in code and tests. - Integration: the replayer's save_healed_to bundle and the teach loop's promoted bundle now register a detached repair/candidate.json (the entry ticket into the lifecycle; failure fails closed by absence). - CLI: openadapt-flow repair register/list/show/review/campaign/approve/ stage/canary/canary-record/rollback/status. - Docs: docs/REPAIR_LIFECYCLE.md (state machine + CLI reference); README pointer. - Also: ruff-format openadapt_flow/transaction.py and tests/test_transaction_outcome.py so the required format gate is green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NyCHrzA1psrKMFfroYbzaM
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements roadmap Section 9 (Govern repair promotion): a proposed repair (healed or taught bundle) is never implicitly active; it moves through an explicit, auditable, fail-closed lifecycle.
State machine
with
rejectedreachable from every pre-staged state,rolled_backfrom canary/active, and automaticcanary -> stagedon any regression. Location:openadapt_flow/repair/lifecycle.py(RepairStore,_ALLOWEDtransition table). Full docs with mermaid diagram + CLI reference:docs/REPAIR_LIFECYCLE.md.What this adds
openadapt_flow/repair/candidate.py): the changed binding as per-step field diffs (values digested, never raw), why the old binding failed (evidence refs by path + hash, privacy-safe failure fingerprints with no raw observations), what supports the new binding (campaign results, approval, canary metrics), and full lineage (prior/proposed whole-bundle content digests, environment contract hash as compatibility scope, transition history). Never auto-activates.openadapt_flow/repair/campaign.py): the replay campaign reuses the existing deterministic drift battery (openadapt_flow/runtime/healing/perturbation.py) over healthy cases (must locate + verify identity on every case); the fault campaign adds a deterministic adversarial battery (ambiguity, wrong entity, stale target, unexpected dialog, verifier failure) where every case must be REFUSED (acting confidently on any fault frame fails the campaign; an unarmed binding that acts always fails). Results recorded on the candidate; approval refuses until both pass.openadapt_flow/repair/invariants.py): diffs the identity / effect / risk / environment / policy contract fields of the two bundles and hard-refuses (fail closed) any weakening unless the proposed bundle carries an explicit new qualification revision (revision strictly advanced + chained viaprevious_revision_sha256). Checked at registration AND re-checked at approval against the bundles on disk.openadapt-flow repair approve <id>requires a human identity;--non-interactive(automation) requires an explicit--approved-by. The approval binds exact content digests; staging copies BOTH bundles into the store by hash and re-verifies byte-exact (prior too, so rollback is always local); activation is an atomicACTIVE.jsonpointer swap (os.replace) after the staged copy re-verifies. One-command rollback:openadapt-flow repair rollback.--max-runs) with per-run verification (repair canary-record, deriving fail-safe observations fromreport.json); any silent-incorrect or verification regression immediately reverts the pointer to the prior digest and drops the candidate back tostaged; N fully verified runs complete toactive.BundleManifest.content_digestthe sealing/integrity path uses), so Desktop/runners/Cloud can sync deterministically; the candidate record itself is privacy-safe by construction and digestable (record_sha256()).assert_not_model_actor,ModelActuationError); review/approve additionally require a human. Amodel_suggestion-sourced candidate flows through the same human-gated path. Asserted in code and tests.Integration with today's heal path
The in-run heal governance (
openadapt_flow/runtime/healing/: identity-never-weakened RegressionGate, quarantine, perturbation harness) is unchanged and still guards each heal during a run. The two places where a proposed bundle used to become implicitly usable now register a detachedrepair/candidate.jsoninside it:Replayer.run(..., save_healed_to=...)(openadapt_flow/runtime/replayer.py)teach()promoted bundles (openadapt_flow/learning/teach.py)Registration failure fails closed by absence: an unregistered proposed bundle cannot enter the lifecycle at all. The in-run gate protects the run; this lifecycle protects the fleet.
CLI
openadapt-flow repair register | list | show | review | campaign --kind replay|fault | approve | stage | canary | canary-record | rollback | status(reference table indocs/REPAIR_LIFECYCLE.md). The production campaign runner wires the real resolver + OCR band sampler; tests inject deterministic fakes through the same seams.Tests
tests/test_repair_lifecycle.py(28 tests, all passing): full lifecycle happy path; each gate refuses (failed fault campaign blocks approval, weakened identity/risk/effect contracts hard-refused, weakening admitted only with an explicit new qualification revision, unapproved promotion refused, tampered staged bundle refuses activation, digest drift after registration refuses approval); canary silent-incorrect and verification regressions auto-revert to the prior hash; rollback restores the prior digest; model actors refused on every transition; automation cannot review/approve; replayer end-to-end heal registers a candidate and never auto-activates; privacy assertion (no raw band/OCR text in the candidate record).Gates
uvx ruff@0.15.22 check openadapt_flowandformat --check openadapt_flow tests: clean (this also ruff-formatsopenadapt_flow/transaction.py+tests/test_transaction_outcome.py, which were failing the required format gate on main after feat(runtime): explicit transaction outcome taxonomy + effect journal + idempotency (Section 3) #259).runtime/replayer.py,qualification.py+ all new repair modules): clean.pytest --ignore=tests/e2e): passing.scripts/check_consistency.py,scripts/check_bundle_phi.py,scripts/validate_claims.py --check: passing.scripts/check_release_consistency.py+--validate-dist-diron a fresh wheel/sdist build: license/private boundary passed.paper/check_artifacts.pyunaffected (no paper changes).DO NOT MERGE without review.
🤖 Generated with Claude Code
https://claude.ai/code/session_01NyCHrzA1psrKMFfroYbzaM