111 lines
3.7 KiB
Python
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
|
|
),
|
|
)
|