feat(ai): verify canonical learning writeback
All checks were successful
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 2m42s
CD Pipeline / build-and-deploy (push) Successful in 6m29s
CD Pipeline / post-deploy-checks (push) Successful in 1m48s
AI 技術雷達監控 / ai-technology-watch (push) Successful in 45s
AWOOOI Harbor 110 Local Repair / workflow-shape (push) Successful in 1s
AWOOOI Harbor 110 Local Repair / harbor-110-local-repair (push) Successful in 50s
All checks were successful
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 2m42s
CD Pipeline / build-and-deploy (push) Successful in 6m29s
CD Pipeline / post-deploy-checks (push) Successful in 1m48s
AI 技術雷達監控 / ai-technology-watch (push) Successful in 45s
AWOOOI Harbor 110 Local Repair / workflow-shape (push) Successful in 1s
AWOOOI Harbor 110 Local Repair / harbor-110-local-repair (push) Successful in 50s
This commit is contained in:
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
@@ -48,6 +49,122 @@ def _json_dict(value: Any) -> dict[str, Any]:
|
||||
return {}
|
||||
|
||||
|
||||
def resolve_ansible_playbook_identity(
|
||||
catalog_id: str,
|
||||
) -> dict[str, Any] | None:
|
||||
"""Resolve one allowlisted catalog action to its canonical PlayBook row."""
|
||||
|
||||
catalog = get_ansible_catalog_item(catalog_id)
|
||||
if not catalog:
|
||||
return None
|
||||
playbook_path = str(catalog.get("playbook_path") or "")
|
||||
canonical_id = canonical_ansible_playbook_id(catalog_id)
|
||||
identity_payload = {
|
||||
"canonical_playbook_id": canonical_id,
|
||||
"catalog_id": catalog_id,
|
||||
"playbook_path": playbook_path,
|
||||
}
|
||||
return {
|
||||
"schema_version": "ansible_playbook_identity_v1",
|
||||
**identity_payload,
|
||||
"identity_fingerprint": hashlib.sha256(
|
||||
json.dumps(
|
||||
identity_payload,
|
||||
ensure_ascii=True,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
).encode("utf-8")
|
||||
).hexdigest(),
|
||||
}
|
||||
|
||||
|
||||
def _playbook_row_fingerprint(playbook: PlaybookRecord) -> str:
|
||||
updated_at = playbook.updated_at.isoformat() if playbook.updated_at else None
|
||||
payload = {
|
||||
"playbook_id": playbook.playbook_id,
|
||||
"project_id": playbook.project_id,
|
||||
"version": int(playbook.version or 0),
|
||||
"success_count": int(playbook.success_count or 0),
|
||||
"failure_count": int(playbook.failure_count or 0),
|
||||
"trust_score": round(float(playbook.trust_score or 0.0), 6),
|
||||
"updated_at": updated_at,
|
||||
}
|
||||
return hashlib.sha256(
|
||||
json.dumps(
|
||||
payload,
|
||||
ensure_ascii=True,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
).encode("utf-8")
|
||||
).hexdigest()
|
||||
|
||||
|
||||
def _playbook_trust_readback(
|
||||
*,
|
||||
playbook: PlaybookRecord,
|
||||
identity: dict[str, Any],
|
||||
verification_result: str,
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"schema_version": "ansible_playbook_trust_writeback_v2",
|
||||
"identity_schema_version": identity["schema_version"],
|
||||
"identity_fingerprint": identity["identity_fingerprint"],
|
||||
"canonical_playbook_id": playbook.playbook_id,
|
||||
"catalog_id": identity["catalog_id"],
|
||||
"playbook_path": identity["playbook_path"],
|
||||
"playbook_row_version": int(playbook.version or 0),
|
||||
"playbook_row_updated_at": (
|
||||
playbook.updated_at.isoformat() if playbook.updated_at else None
|
||||
),
|
||||
"playbook_row_fingerprint": _playbook_row_fingerprint(playbook),
|
||||
"success_count": int(playbook.success_count or 0),
|
||||
"failure_count": int(playbook.failure_count or 0),
|
||||
"trust_observation_count": (
|
||||
int(playbook.success_count or 0)
|
||||
+ int(playbook.failure_count or 0)
|
||||
),
|
||||
"trust_score": round(float(playbook.trust_score or 0.0), 6),
|
||||
"verification_result": verification_result,
|
||||
"learning_recorded": True,
|
||||
"trust_updated": True,
|
||||
"repository_write_acknowledged": True,
|
||||
"repository_readback_verified": True,
|
||||
"operation_receipt_readback_verified": True,
|
||||
"durable_write_acknowledged": True,
|
||||
"source_table": "playbooks",
|
||||
"writer_source_sha": os.getenv("AWOOOI_BUILD_COMMIT_SHA", "")
|
||||
.strip()
|
||||
.lower()
|
||||
or None,
|
||||
"raw_log_payload_stored": False,
|
||||
"secret_value_stored": False,
|
||||
}
|
||||
|
||||
|
||||
def _existing_trust_readback_matches(
|
||||
existing_trust: dict[str, Any],
|
||||
*,
|
||||
playbook: PlaybookRecord,
|
||||
identity: dict[str, Any],
|
||||
) -> bool:
|
||||
return bool(
|
||||
existing_trust.get("schema_version")
|
||||
== "ansible_playbook_trust_writeback_v2"
|
||||
and existing_trust.get("canonical_playbook_id")
|
||||
== identity["canonical_playbook_id"]
|
||||
and existing_trust.get("identity_fingerprint")
|
||||
== identity["identity_fingerprint"]
|
||||
and existing_trust.get("playbook_row_version")
|
||||
== int(playbook.version or 0)
|
||||
and existing_trust.get("playbook_row_fingerprint")
|
||||
== _playbook_row_fingerprint(playbook)
|
||||
and existing_trust.get("repository_readback_verified") is True
|
||||
and existing_trust.get("operation_receipt_readback_verified") is True
|
||||
and existing_trust.get("learning_recorded") is True
|
||||
and existing_trust.get("trust_updated") is True
|
||||
)
|
||||
|
||||
|
||||
async def record_ansible_playbook_trust_writeback(
|
||||
*,
|
||||
project_id: str,
|
||||
@@ -62,9 +179,20 @@ async def record_ansible_playbook_trust_writeback(
|
||||
"""Atomically update canonical PlayBook trust and its learning receipt."""
|
||||
|
||||
catalog = get_ansible_catalog_item(catalog_id)
|
||||
if not catalog:
|
||||
identity = resolve_ansible_playbook_identity(catalog_id)
|
||||
if not catalog or identity is None:
|
||||
return None
|
||||
canonical_id = canonical_ansible_playbook_id(catalog_id)
|
||||
canonical_id = str(identity["canonical_playbook_id"])
|
||||
canonical_playbook_path = str(identity["playbook_path"])
|
||||
if str(playbook_path) != canonical_playbook_path:
|
||||
logger.warning(
|
||||
"ansible_learning_playbook_identity_mismatch",
|
||||
incident_id=incident_id,
|
||||
catalog_id=catalog_id,
|
||||
apply_op_id=apply_op_id,
|
||||
)
|
||||
return None
|
||||
playbook_path = canonical_playbook_path
|
||||
success = verification_result == "success"
|
||||
now = now_taipei()
|
||||
|
||||
@@ -89,11 +217,6 @@ async def record_ansible_playbook_trust_writeback(
|
||||
existing_trust = _json_dict(
|
||||
existing_output.get("playbook_trust_writeback")
|
||||
)
|
||||
if (
|
||||
existing_trust.get("durable_write_acknowledged") is True
|
||||
and existing_trust.get("canonical_playbook_id") == canonical_id
|
||||
):
|
||||
return existing_trust
|
||||
|
||||
selected = await db.execute(
|
||||
select(PlaybookRecord)
|
||||
@@ -101,6 +224,31 @@ async def record_ansible_playbook_trust_writeback(
|
||||
.with_for_update()
|
||||
)
|
||||
playbook = selected.scalar_one_or_none()
|
||||
if existing_row:
|
||||
if playbook is None:
|
||||
logger.warning(
|
||||
"ansible_learning_receipt_playbook_row_missing",
|
||||
incident_id=incident_id,
|
||||
catalog_id=catalog_id,
|
||||
apply_op_id=apply_op_id,
|
||||
)
|
||||
return None
|
||||
if _existing_trust_readback_matches(
|
||||
existing_trust,
|
||||
playbook=playbook,
|
||||
identity=identity,
|
||||
):
|
||||
return existing_trust
|
||||
logger.warning(
|
||||
"ansible_learning_receipt_readback_not_verified",
|
||||
incident_id=incident_id,
|
||||
catalog_id=catalog_id,
|
||||
apply_op_id=apply_op_id,
|
||||
receipt_schema_version=existing_trust.get(
|
||||
"schema_version"
|
||||
),
|
||||
)
|
||||
return None
|
||||
if playbook is None:
|
||||
playbook = PlaybookRecord(
|
||||
playbook_id=canonical_id,
|
||||
@@ -161,6 +309,11 @@ async def record_ansible_playbook_trust_writeback(
|
||||
db.add(playbook)
|
||||
await db.flush()
|
||||
|
||||
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)
|
||||
playbook.source_incident_ids = source_incident_ids
|
||||
|
||||
if success:
|
||||
playbook.success_count = int(playbook.success_count or 0) + 1
|
||||
playbook.trust_score = 0.9 * float(playbook.trust_score) + 0.1
|
||||
@@ -170,84 +323,100 @@ async def record_ansible_playbook_trust_writeback(
|
||||
playbook.trust_score = max(0.0, min(1.0, playbook.trust_score))
|
||||
playbook.last_used_at = now
|
||||
playbook.updated_at = now
|
||||
|
||||
trust_writeback = {
|
||||
"schema_version": "ansible_playbook_trust_writeback_v1",
|
||||
"canonical_playbook_id": canonical_id,
|
||||
"catalog_id": catalog_id,
|
||||
"success_count": playbook.success_count,
|
||||
"failure_count": playbook.failure_count,
|
||||
"trust_score": round(float(playbook.trust_score), 6),
|
||||
"verification_result": verification_result,
|
||||
"durable_write_acknowledged": True,
|
||||
"source_table": "playbooks",
|
||||
}
|
||||
await db.flush()
|
||||
await db.refresh(playbook)
|
||||
trust_writeback = _playbook_trust_readback(
|
||||
playbook=playbook,
|
||||
identity=identity,
|
||||
verification_result=verification_result,
|
||||
)
|
||||
input_payload = {
|
||||
"schema_version": "ansible_learning_writeback_receipt_v2",
|
||||
"schema_version": "ansible_learning_writeback_receipt_v3",
|
||||
"automation_run_id": automation_run_id,
|
||||
"incident_id": incident_id,
|
||||
"catalog_id": catalog_id,
|
||||
"canonical_playbook_id": canonical_id,
|
||||
"identity_schema_version": identity["schema_version"],
|
||||
"identity_fingerprint": identity["identity_fingerprint"],
|
||||
"playbook_path": playbook_path,
|
||||
"apply_op_id": apply_op_id,
|
||||
"verification_result": verification_result,
|
||||
"learning_repository": "playbooks",
|
||||
"writer_source_sha": trust_writeback["writer_source_sha"],
|
||||
"stores_raw_logs": False,
|
||||
"stores_secret_values": False,
|
||||
}
|
||||
output_payload = {
|
||||
"learning_recorded": True,
|
||||
"trust_updated": True,
|
||||
"success": success,
|
||||
"repository_readback_verified": True,
|
||||
"playbook_trust_writeback": trust_writeback,
|
||||
}
|
||||
if existing_row:
|
||||
await db.execute(
|
||||
text("""
|
||||
UPDATE automation_operation_log
|
||||
SET status = 'success',
|
||||
input = coalesce(input, '{}'::jsonb) || CAST(:input AS jsonb),
|
||||
output = coalesce(output, '{}'::jsonb) || CAST(:output AS jsonb),
|
||||
error = NULL
|
||||
WHERE op_id = CAST(:op_id AS uuid)
|
||||
"""),
|
||||
{
|
||||
"input": json.dumps(input_payload, ensure_ascii=False),
|
||||
"output": json.dumps(output_payload, ensure_ascii=False),
|
||||
"op_id": str(existing_row["op_id"]),
|
||||
},
|
||||
)
|
||||
else:
|
||||
await db.execute(
|
||||
text("""
|
||||
INSERT INTO automation_operation_log (
|
||||
operation_type, actor, status, incident_id,
|
||||
input, output, dry_run_result, parent_op_id, tags
|
||||
) VALUES (
|
||||
'ansible_learning_writeback_recorded',
|
||||
'ansible_controlled_apply_worker',
|
||||
'success',
|
||||
:incident_db_id,
|
||||
CAST(:input AS jsonb),
|
||||
CAST(:output AS jsonb),
|
||||
'{}'::jsonb,
|
||||
CAST(:parent_op_id AS uuid),
|
||||
:tags
|
||||
)
|
||||
"""),
|
||||
{
|
||||
"incident_db_id": incident_db_id,
|
||||
"input": json.dumps(input_payload, ensure_ascii=False),
|
||||
"output": json.dumps(output_payload, ensure_ascii=False),
|
||||
"parent_op_id": apply_op_id,
|
||||
"tags": [
|
||||
"ansible",
|
||||
"controlled_apply",
|
||||
"learning_writeback",
|
||||
"playbook_trust",
|
||||
"ai_agent_auto_execution",
|
||||
f"automation_run_id:{automation_run_id}",
|
||||
],
|
||||
},
|
||||
await db.execute(
|
||||
text("""
|
||||
INSERT INTO automation_operation_log (
|
||||
operation_type, actor, status, incident_id,
|
||||
input, output, dry_run_result, parent_op_id, tags
|
||||
) VALUES (
|
||||
'ansible_learning_writeback_recorded',
|
||||
'ansible_controlled_apply_worker',
|
||||
'success',
|
||||
:incident_db_id,
|
||||
CAST(:input AS jsonb),
|
||||
CAST(:output AS jsonb),
|
||||
'{}'::jsonb,
|
||||
CAST(:parent_op_id AS uuid),
|
||||
:tags
|
||||
)
|
||||
"""),
|
||||
{
|
||||
"incident_db_id": incident_db_id,
|
||||
"input": json.dumps(input_payload, ensure_ascii=False),
|
||||
"output": json.dumps(output_payload, ensure_ascii=False),
|
||||
"parent_op_id": apply_op_id,
|
||||
"tags": [
|
||||
"ansible",
|
||||
"controlled_apply",
|
||||
"learning_writeback",
|
||||
"playbook_trust",
|
||||
"ai_agent_auto_execution",
|
||||
f"automation_run_id:{automation_run_id}",
|
||||
],
|
||||
},
|
||||
)
|
||||
await db.flush()
|
||||
operation_readback = await db.execute(
|
||||
text("""
|
||||
SELECT status, input, output
|
||||
FROM automation_operation_log
|
||||
WHERE operation_type
|
||||
= 'ansible_learning_writeback_recorded'
|
||||
AND parent_op_id = CAST(:apply_op_id AS uuid)
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
"""),
|
||||
{"apply_op_id": apply_op_id},
|
||||
)
|
||||
operation_row = operation_readback.mappings().first()
|
||||
operation_output = _json_dict(
|
||||
operation_row.get("output") if operation_row else None
|
||||
)
|
||||
operation_trust = _json_dict(
|
||||
operation_output.get("playbook_trust_writeback")
|
||||
)
|
||||
if not (
|
||||
operation_row
|
||||
and operation_row.get("status") == "success"
|
||||
and operation_output.get("learning_recorded") is True
|
||||
and operation_output.get("trust_updated") is True
|
||||
and operation_output.get("repository_readback_verified")
|
||||
is True
|
||||
and operation_trust.get("playbook_row_fingerprint")
|
||||
== trust_writeback["playbook_row_fingerprint"]
|
||||
):
|
||||
raise RuntimeError(
|
||||
"ansible_learning_operation_receipt_readback_failed"
|
||||
)
|
||||
return trust_writeback
|
||||
except Exception as exc:
|
||||
|
||||
Reference in New Issue
Block a user