Files
awoooi/apps/api/src/services/agent99_public_receipts.py

111 lines
3.7 KiB
Python

"""Strict public receipt identifiers shared by every Agent99 readback path.
Receipt references are durable identifiers, never log bodies, filesystem
paths, URLs with query strings, credentials, or arbitrary caller metadata.
Keeping the grammar in one module prevents the webhook and relay reconciler
from accepting different evidence for the same run.
"""
from __future__ import annotations
import re
from collections.abc import Iterable, Mapping
from typing import Any
AGENT99_REQUIRED_VERIFIER_EVIDENCE_REFS = frozenset({
"agent99_outcome_receipt_id",
"post_verifier_evidence_ref",
"source_event_evidence_ref",
})
AGENT99_BACKUP_REQUIRED_VERIFIER_EVIDENCE_REFS = frozenset({
"backup_status_evidence_ref",
"freshness_evidence_ref",
"offsite_verify_evidence_ref",
"escrow_evidence_ref",
"restore_drill_evidence_ref",
"source_resolution_receipt_ref",
})
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",
})
AGENT99_PUBLIC_RECEIPT_REF_KEYS = (
AGENT99_REQUIRED_VERIFIER_EVIDENCE_REFS
| AGENT99_BACKUP_REQUIRED_VERIFIER_EVIDENCE_REFS
| AGENT99_LEARNING_RECEIPT_REFS
)
# URN-like public identifiers only. Deliberately excludes whitespace,
# backslashes, query delimiters, fragments, and assignment (``=``).
_PUBLIC_RECEIPT_REF_RE = re.compile(
r"^[A-Za-z0-9][A-Za-z0-9._:/@+%-]{0,255}$"
)
_SECRET_SHAPED_RE = re.compile(
r"(?:authorization|bearer|password|passwd|token|secret|api[_-]?key|"
r"private[_-]?key|cookie|session)(?:\s|:|=)|"
r"-----BEGIN[^-]*PRIVATE KEY-----|(?:^|[/.])\.env(?:$|[/.])",
re.IGNORECASE,
)
def normalize_agent99_public_receipt_ref(value: Any) -> str | None:
"""Return one bounded public receipt id, or ``None`` when unsafe."""
ref = str(value or "").strip()
if not ref or len(ref) > 256 or not ref.isascii():
return None
if _SECRET_SHAPED_RE.search(ref) or not _PUBLIC_RECEIPT_REF_RE.fullmatch(ref):
return None
if ".." in ref or "://" in ref:
return None
return ref
def sanitize_agent99_public_receipt_refs(
*sources: Mapping[str, Any] | None,
allowed_keys: Iterable[str] = AGENT99_PUBLIC_RECEIPT_REF_KEYS,
) -> dict[str, str]:
"""Merge allowlisted refs using the same strict grammar everywhere."""
allowed = frozenset(str(key) for key in allowed_keys)
safe: dict[str, str] = {}
for source in sources:
for raw_key, raw_value in (source or {}).items():
key = str(raw_key or "").strip()
if key not in allowed:
continue
ref = normalize_agent99_public_receipt_ref(raw_value)
if ref is not None:
safe[key] = ref
return safe
def extract_agent99_outcome_evidence_refs(
outcome_receipt: Mapping[str, Any] | None,
*additional_sources: Mapping[str, Any] | None,
) -> dict[str, str]:
"""Extract generic and BackupCheck refs from one outcome envelope."""
receipt = outcome_receipt or {}
outcome = receipt.get("outcome")
if not isinstance(outcome, Mapping):
outcome = receipt
nested_refs = outcome.get("evidenceRefs")
if not isinstance(nested_refs, Mapping):
nested_refs = outcome.get("evidence_refs")
if not isinstance(nested_refs, Mapping):
nested_refs = {}
return sanitize_agent99_public_receipt_refs(
*additional_sources,
nested_refs,
allowed_keys=(
AGENT99_REQUIRED_VERIFIER_EVIDENCE_REFS
| AGENT99_BACKUP_REQUIRED_VERIFIER_EVIDENCE_REFS
),
)