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
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:
@@ -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 ""
|
||||
),
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
@@ -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"],
|
||||
|
||||
Reference in New Issue
Block a user