The two catch-all handlers added in dac6e97 (r8_fault.record evidence
write, notify_event_bridge seam probe) tripped the #126 silent-handler
ratchet (S110 75 -> 77), which cascaded to the lint-baseline contract
test. Both handlers now log to stderr/stdout and continue — the seam
stays fail-open but is no longer silent. S110 back to 75=75.
84 lines
3.1 KiB
Python
84 lines
3.1 KiB
Python
#!/usr/bin/env python3
|
|
"""r8_fault.py — REAL_ASYNC_BRIDGE R8: operation-scoped lost-event fault
|
|
injection (evidence-only harness).
|
|
|
|
DESIGN CONTRACT (R8 §3):
|
|
disabled by default no rule file -> zero behavior change
|
|
operation-scoped rule keyed by EXACT operation_id
|
|
reversible delete the rule file / rule entry
|
|
observable every decision appended to a JSONL log
|
|
cannot affect other waits rule file checked by operation_id only
|
|
|
|
The ONLY consumer is scripts/notify_event_bridge.py: before normal
|
|
delivery it consults should_suppress(operation_id). Suppression means:
|
|
- NO durable completion file is written
|
|
- NO bridge POST (no /process-event wake)
|
|
- the suppression decision is recorded as R8 fault evidence
|
|
Nothing else in the stack reads the rule. The daemon, waitstore,
|
|
authority verification, and completion-file machinery are untouched.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
RUNE_WR_ROOT = os.environ.get("RUNE_WR_ROOT") or os.environ.get("WR_ROOT") or "/mnt/workdrive/wr-bridge"
|
|
RULES_PATH = Path(RUNE_WR_ROOT) / "r8_suppress_rules.json"
|
|
FAULT_LOG = Path(RUNE_WR_ROOT) / "r8_fault_log.jsonl"
|
|
|
|
REASON = "event intentionally suppressed for R8 lost-event failure-recovery"
|
|
|
|
|
|
def _rules() -> dict:
|
|
"""Read the rule file. Absent/unreadable => no suppression (fail-open
|
|
to NORMAL delivery; the fault must never take the healthy path down)."""
|
|
try:
|
|
return json.loads(RULES_PATH.read_text())
|
|
except Exception: # noqa: BLE001 — absent rule file is the default state
|
|
return {}
|
|
|
|
|
|
def lookup(operation_id: str) -> dict | None:
|
|
"""Return the active suppression rule for EXACTLY this operation_id,
|
|
or None. Expired rules are ignored (not deleted — deletion is the
|
|
operator's explicit disable action)."""
|
|
spec = _rules()
|
|
if not spec.get("enabled"):
|
|
return None
|
|
rule = spec.get("operations", {}).get(operation_id)
|
|
if not rule:
|
|
return None
|
|
expires = rule.get("expires_at")
|
|
if expires:
|
|
try:
|
|
if time.time() > time.mktime(time.strptime(expires, "%Y-%m-%dT%H:%M:%SZ")) - time.timezone:
|
|
return None
|
|
except ValueError:
|
|
return None
|
|
return rule
|
|
|
|
|
|
def should_suppress(operation_id: str) -> bool:
|
|
return lookup(operation_id) is not None
|
|
|
|
|
|
def record(operation_id: str, stage: str, extra: dict | None = None) -> None:
|
|
"""Append a durable fault-evidence line (best-effort, never raises)."""
|
|
try:
|
|
entry = {
|
|
"at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
"operation_id": operation_id,
|
|
"reason": REASON,
|
|
"stage": stage,
|
|
**(extra or {}),
|
|
}
|
|
FAULT_LOG.parent.mkdir(parents=True, exist_ok=True)
|
|
with FAULT_LOG.open("a") as f:
|
|
f.write(json.dumps(entry, sort_keys=True) + "\n")
|
|
except OSError as exc: # noqa: BLE001 — evidence logging must never break CI
|
|
print(f"r8_fault: evidence log write failed: {exc}", file=sys.stderr)
|