feat(sre): require Agent99 RAG and MCP closure receipts
This commit is contained in:
@@ -4,13 +4,17 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
from collections.abc import Awaitable, Callable
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
from uuid import NAMESPACE_URL, uuid5
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import select, text
|
||||
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
||||
|
||||
from src.db.awooop_models import AwoooPMcpGatewayAudit
|
||||
from src.db.base import get_db_context
|
||||
from src.db.models import PlaybookRecord
|
||||
from src.models.knowledge import (
|
||||
@@ -438,6 +442,140 @@ async def _ensure_km_writeback(
|
||||
return None
|
||||
|
||||
|
||||
async def _ensure_rag_writeback(
|
||||
identity: Agent99DispatchIdentity,
|
||||
) -> str | None:
|
||||
"""Read back the durable embedding created by the Agent99 KM writer."""
|
||||
|
||||
try:
|
||||
async with get_db_context(identity.project_id) as db:
|
||||
result = await db.execute(
|
||||
text("""
|
||||
SELECT id::text AS entry_id,
|
||||
embedding IS NOT NULL AS embedding_persisted
|
||||
FROM knowledge_entries
|
||||
WHERE project_id = :project_id
|
||||
AND related_incident_id = :incident_id
|
||||
AND path_type = :path_type
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
"""),
|
||||
{
|
||||
"project_id": identity.project_id,
|
||||
"incident_id": identity.incident_id,
|
||||
"path_type": f"agent99:{identity.run_id}",
|
||||
},
|
||||
)
|
||||
row = result.mappings().first()
|
||||
if not row or row.get("embedding_persisted") is not True:
|
||||
return None
|
||||
return f"knowledge_entries:{row['entry_id']}:rag_embedding_verified"
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"agent99_rag_writeback_readback_failed",
|
||||
run_id=str(identity.run_id),
|
||||
error=str(exc),
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def _mcp_learning_call_id(identity: Agent99DispatchIdentity):
|
||||
return uuid5(
|
||||
NAMESPACE_URL,
|
||||
f"agent99-mcp-learning:{identity.project_id}:{identity.run_id}",
|
||||
)
|
||||
|
||||
|
||||
async def _ensure_mcp_evidence_writeback(
|
||||
identity: Agent99DispatchIdentity,
|
||||
*,
|
||||
receipt_refs: dict[str, str],
|
||||
) -> str | None:
|
||||
"""Publish and read back a public-safe same-run MCP evidence binding."""
|
||||
|
||||
required = {
|
||||
"telegram_lifecycle_receipt_id",
|
||||
"km_writeback_ack_id",
|
||||
"rag_writeback_ack_id",
|
||||
"playbook_trust_writeback_ack_id",
|
||||
}
|
||||
safe_refs = sanitize_agent99_public_receipt_refs(
|
||||
receipt_refs,
|
||||
allowed_keys=required,
|
||||
)
|
||||
if not required.issubset(safe_refs):
|
||||
return None
|
||||
bindings = {key: safe_refs[key] for key in sorted(required)}
|
||||
identity_hash = hashlib.sha256(
|
||||
json.dumps(
|
||||
identity.public_dict(),
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
).encode()
|
||||
).hexdigest()
|
||||
bindings_hash = hashlib.sha256(
|
||||
json.dumps(bindings, sort_keys=True, separators=(",", ":")).encode()
|
||||
).hexdigest()
|
||||
call_id = _mcp_learning_call_id(identity)
|
||||
gate_result = {
|
||||
"schema_version": "agent99_mcp_learning_evidence_v1",
|
||||
"incident_id": identity.incident_id,
|
||||
"route_id": identity.route_id,
|
||||
"receipt_refs": bindings,
|
||||
"raw_evidence_stored": False,
|
||||
"secret_value_stored": False,
|
||||
}
|
||||
try:
|
||||
async with get_db_context(identity.project_id) as db:
|
||||
await db.execute(
|
||||
pg_insert(AwoooPMcpGatewayAudit)
|
||||
.values(
|
||||
call_id=call_id,
|
||||
project_id=identity.project_id,
|
||||
run_id=identity.run_id,
|
||||
trace_id=identity.trace_id,
|
||||
agent_id="agent99_controlled_dispatch_reconciler",
|
||||
tool_name="agent99_learning_evidence_publish",
|
||||
input_hash=identity_hash,
|
||||
output_hash=bindings_hash,
|
||||
gate_result=gate_result,
|
||||
result_status="success",
|
||||
latency_ms=0,
|
||||
)
|
||||
.on_conflict_do_nothing(
|
||||
index_elements=[AwoooPMcpGatewayAudit.call_id]
|
||||
)
|
||||
)
|
||||
selected = await db.execute(
|
||||
select(AwoooPMcpGatewayAudit).where(
|
||||
AwoooPMcpGatewayAudit.call_id == call_id,
|
||||
AwoooPMcpGatewayAudit.project_id == identity.project_id,
|
||||
AwoooPMcpGatewayAudit.run_id == identity.run_id,
|
||||
)
|
||||
)
|
||||
row = selected.scalar_one_or_none()
|
||||
if not (
|
||||
row
|
||||
and str(row.trace_id or "") == identity.trace_id
|
||||
and row.agent_id == "agent99_controlled_dispatch_reconciler"
|
||||
and row.tool_name == "agent99_learning_evidence_publish"
|
||||
and row.input_hash == identity_hash
|
||||
and row.output_hash == bindings_hash
|
||||
and row.result_status == "success"
|
||||
and isinstance(row.gate_result, dict)
|
||||
and row.gate_result == gate_result
|
||||
):
|
||||
return None
|
||||
return f"awooop_mcp_gateway_audit:{call_id}:verified"
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"agent99_mcp_learning_evidence_writeback_failed",
|
||||
run_id=str(identity.run_id),
|
||||
error=str(exc),
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
async def _ensure_telegram_receipt(
|
||||
identity: Agent99DispatchIdentity,
|
||||
*,
|
||||
@@ -575,10 +713,21 @@ async def _finalize_learning(
|
||||
"km_writeback_ack_id",
|
||||
lambda: _ensure_km_writeback(identity, mode=mode),
|
||||
),
|
||||
(
|
||||
"rag_writeback_ack_id",
|
||||
lambda: _ensure_rag_writeback(identity),
|
||||
),
|
||||
(
|
||||
"telegram_lifecycle_receipt_id",
|
||||
lambda: _ensure_telegram_receipt(identity, mode=mode),
|
||||
),
|
||||
(
|
||||
"mcp_evidence_writeback_ack_id",
|
||||
lambda: _ensure_mcp_evidence_writeback(
|
||||
identity,
|
||||
receipt_refs=receipt_refs,
|
||||
),
|
||||
),
|
||||
]
|
||||
if "backup_health" in identity.route_id:
|
||||
assets.append((
|
||||
|
||||
@@ -498,6 +498,8 @@ def build_agent99_dispatch_receipt_envelope(
|
||||
"incident_closure_receipt_id",
|
||||
"telegram_lifecycle_receipt_id",
|
||||
"km_writeback_ack_id",
|
||||
"rag_writeback_ack_id",
|
||||
"mcp_evidence_writeback_ack_id",
|
||||
"playbook_trust_writeback_ack_id",
|
||||
],
|
||||
},
|
||||
@@ -1406,6 +1408,8 @@ class PostgresAgent99DispatchLedger:
|
||||
"incident_closure_receipt_id",
|
||||
"telegram_lifecycle_receipt_id",
|
||||
"km_writeback_ack_id",
|
||||
"rag_writeback_ack_id",
|
||||
"mcp_evidence_writeback_ack_id",
|
||||
"playbook_trust_writeback_ack_id",
|
||||
],
|
||||
},
|
||||
@@ -2068,15 +2072,17 @@ class PostgresAgent99DispatchLedger:
|
||||
) -> dict[str, Any]:
|
||||
"""Atomically close the incident, operation log, and same-run ledger.
|
||||
|
||||
KM, PlayBook, DR, and Telegram each provide a durable idempotent
|
||||
acknowledgement first. The incident terminal state, immutable closure
|
||||
event, run state, and step-3 receipt then commit in one PostgreSQL
|
||||
transaction. Redis is only a post-commit projection.
|
||||
KM, RAG, MCP evidence, PlayBook, DR, and Telegram each provide a
|
||||
durable idempotent acknowledgement first. The incident terminal state,
|
||||
immutable closure event, run state, and step-3 receipt then commit in
|
||||
one PostgreSQL transaction. Redis is only a post-commit projection.
|
||||
"""
|
||||
|
||||
required_refs = {
|
||||
"telegram_lifecycle_receipt_id",
|
||||
"km_writeback_ack_id",
|
||||
"rag_writeback_ack_id",
|
||||
"mcp_evidence_writeback_ack_id",
|
||||
"playbook_trust_writeback_ack_id",
|
||||
}
|
||||
if "backup_health" in identity.route_id:
|
||||
@@ -2354,11 +2360,12 @@ class PostgresAgent99DispatchLedger:
|
||||
) -> dict[str, Any]:
|
||||
"""Checkpoint per-run learning side effects before terminal closure.
|
||||
|
||||
Reconciliation may crash after KM, PlayBook, Telegram, or DR succeeds.
|
||||
Persisting each public acknowledgement on the same run lets the next
|
||||
tick resume at the first missing asset instead of replaying completed
|
||||
side effects. The asset writers retain their own deterministic keys as
|
||||
the final crash fence between side effect and this checkpoint.
|
||||
Reconciliation may crash after KM, RAG, MCP evidence, PlayBook,
|
||||
Telegram, or DR succeeds. Persisting each public acknowledgement on
|
||||
the same run lets the next tick resume at the first missing asset
|
||||
instead of replaying completed side effects. The asset writers retain
|
||||
their own deterministic keys as the final crash fence between side
|
||||
effect and this checkpoint.
|
||||
"""
|
||||
|
||||
supplied = sanitize_agent99_public_receipt_refs(
|
||||
|
||||
@@ -29,6 +29,8 @@ AGENT99_LEARNING_RECEIPT_REFS = frozenset({
|
||||
"incident_closure_receipt_id",
|
||||
"telegram_lifecycle_receipt_id",
|
||||
"km_writeback_ack_id",
|
||||
"rag_writeback_ack_id",
|
||||
"mcp_evidence_writeback_ack_id",
|
||||
"playbook_trust_writeback_ack_id",
|
||||
"dr_scorecard_writeback_ack_id",
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user