fix(agent): canary repaired playbook revisions
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 1s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / build-and-deploy (push) Has been cancelled
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / tests (push) Has been cancelled

This commit is contained in:
ogt
2026-07-14 19:52:35 +08:00
parent 177040c273
commit dbf8d0255e
4 changed files with 249 additions and 6 deletions

View File

@@ -252,7 +252,7 @@ def _apply_ansible_playbook_trust_gate(
candidate_input: dict[str, Any],
trust_rows: list[Mapping[str, Any]],
) -> tuple[dict[str, Any], dict[str, Any]]:
"""Filter failed PlayBooks before they can claim another runtime apply."""
"""Filter failed PlayBooks while allowing one repaired revision canary."""
candidates = candidate_input.get("executor_candidates")
if not isinstance(candidates, list):
@@ -274,16 +274,44 @@ def _apply_ansible_playbook_trust_gate(
trust_score = float((row or {}).get("trust_score") or 0.0)
success_count = int((row or {}).get("success_count") or 0)
failure_count = int((row or {}).get("failure_count") or 0)
catalog_item = get_ansible_catalog_item(catalog_id) or {}
candidate_catalog_revision = str(
candidate.get("catalog_revision") or ""
).strip()
allowlisted_catalog_revision = str(
catalog_item.get("catalog_revision") or ""
).strip()
current_catalog_revision = str(
allowlisted_catalog_revision
or candidate_catalog_revision
or ""
).strip()
observed_pattern = _json_loads(
(row or {}).get("symptom_pattern")
)
observed_catalog_revision = str(
observed_pattern.get("catalog_revision") or ""
).strip()
retired = status in {"deprecated", "archived"}
learned_failure = bool(
row
and failure_count > success_count
and trust_score < TRUST_ARCHIVE_THRESHOLD
)
circuit_open = retired or learned_failure
bounded_repair_canary = bool(
not retired
and learned_failure
and current_catalog_revision
and current_catalog_revision != observed_catalog_revision
)
circuit_open = retired or (
learned_failure and not bounded_repair_canary
)
reason = (
"playbook_status_retired"
if retired
else "repaired_catalog_revision_canary_allowed"
if bounded_repair_canary
else "playbook_trust_below_archive_threshold"
if learned_failure
else "playbook_trust_allows_controlled_canary"
@@ -300,6 +328,22 @@ def _apply_ansible_playbook_trust_gate(
"success_count": success_count,
"failure_count": failure_count,
"review_required": bool((row or {}).get("review_required")),
"current_catalog_revision": current_catalog_revision or None,
"catalog_revision_source": (
"allowlisted_catalog"
if allowlisted_catalog_revision
else "candidate_fallback"
if candidate_catalog_revision
else None
),
"candidate_catalog_revision_mismatch": bool(
allowlisted_catalog_revision
and candidate_catalog_revision
and candidate_catalog_revision
!= allowlisted_catalog_revision
),
"observed_catalog_revision": observed_catalog_revision or None,
"bounded_repair_canary": bounded_repair_canary,
"circuit_open": circuit_open,
"reason": reason,
}
@@ -316,7 +360,7 @@ def _apply_ansible_playbook_trust_gate(
else "closed"
)
gate = {
"schema_version": "ansible_playbook_trust_claim_gate_v1",
"schema_version": "ansible_playbook_trust_claim_gate_v2",
"policy_source": "playbook_evolver.TRUST_ARCHIVE_THRESHOLD",
"trust_archive_threshold": TRUST_ARCHIVE_THRESHOLD,
"status": status,
@@ -363,6 +407,7 @@ async def _guard_candidate_input_by_playbook_trust(
PlaybookRecord.success_count,
PlaybookRecord.failure_count,
PlaybookRecord.review_required,
PlaybookRecord.symptom_pattern,
).where(PlaybookRecord.playbook_id.in_(canonical_ids))
)
trust_rows = list(result.mappings().all())
@@ -382,6 +427,9 @@ async def _revalidate_claim_playbook_trust(
"check_mode_playbook_path": claim.playbook_path,
"inventory_hosts": list(claim.inventory_hosts),
"risk_level": claim.risk_level,
"catalog_revision": str(
claim.input_payload.get("catalog_revision") or ""
),
}
],
}

View File

@@ -49,6 +49,28 @@ def _json_dict(value: Any) -> dict[str, Any]:
return {}
def _playbook_catalog_revision(playbook: PlaybookRecord) -> str | None:
revision = str(
_json_dict(playbook.symptom_pattern).get("catalog_revision") or ""
).strip()
return revision or None
def _record_catalog_revision_observation(
playbook: PlaybookRecord,
catalog: dict[str, Any],
) -> str | None:
"""Bind a trust mutation to the catalog revision it observed."""
revision = str(catalog.get("catalog_revision") or "").strip()
if not revision:
return None
symptom_pattern = dict(_json_dict(playbook.symptom_pattern))
symptom_pattern["catalog_revision"] = revision
playbook.symptom_pattern = symptom_pattern
return revision
def resolve_ansible_playbook_identity(
catalog_id: str,
) -> dict[str, Any] | None:
@@ -117,6 +139,7 @@ def _playbook_trust_readback(
"identity_fingerprint": identity["identity_fingerprint"],
"canonical_playbook_id": playbook.playbook_id,
"catalog_id": identity["catalog_id"],
"catalog_revision": _playbook_catalog_revision(playbook),
"playbook_path": identity["playbook_path"],
"playbook_row_version": int(playbook.version or 0),
"playbook_row_updated_at": (
@@ -301,6 +324,10 @@ async def record_ansible_playbook_trust_writeback(
"severity_range": ["P0", "P1", "P2", "P3"],
"label_patterns": {},
"keywords": list(catalog.get("keywords") or [])[:50],
"catalog_revision": str(
catalog.get("catalog_revision") or ""
)
or None,
},
repair_steps=[
{
@@ -345,6 +372,7 @@ async def record_ansible_playbook_trust_writeback(
await db.flush()
if trust_mutation_performed:
_record_catalog_revision_observation(playbook, catalog)
source_incident_ids = list(playbook.source_incident_ids or [])
if incident_id and incident_id not in source_incident_ids:
source_incident_ids.append(incident_id)
@@ -373,6 +401,7 @@ async def record_ansible_playbook_trust_writeback(
"automation_run_id": automation_run_id,
"incident_id": incident_id,
"catalog_id": catalog_id,
"catalog_revision": trust_writeback["catalog_revision"],
"canonical_playbook_id": canonical_id,
"identity_schema_version": identity["schema_version"],
"identity_fingerprint": identity["identity_fingerprint"],