Files
VianoBugAnalysis/src/viano_bug_analysis/telemetry/safety.py
T

131 lines
8.1 KiB
Python

import errno
import re
import traceback
from .codes import KNOWN_CODES, KNOWN_MODULES, KNOWN_CLASSES
import math
EVENTS = frozenset({"observer_started", "observer_stopped", "operation_started", "operation_finished", "operation_failed", "telemetry_read_warning",
"case_checkpoint", "llm_round", "phase_diagnostic", "inventory_updated", "telemetry_write_failed"})
OPERATIONS = frozenset({"case_build", "case_interpret", "hypotheses_build", "investigation_run", "investigation_step",
"diagnosis_build", "change_request_build", "output_validate", "output_finalize", "output_publish",
"llm_run", "checkpoint_advance", "intake", "inbox_poll", "inventory_scan", "budget_execute",
"evidence_execute", "async_test_submit", "async_test_confirm"})
STATUSES = frozenset({"SUCCESS", "ERROR", "BUSY", "CANCELLED", "COMPLETED", "INCONCLUSIVE", "BLOCKED", "RETRY_PENDING",
"DEFERRED", "PROGRESSED", "REJECTED", "YIELDED", "ACCEPTED", "DUPLICATE", "RESUMED", "QUARANTINED",
"VALID", "REVIEW", "OUTPUT_READY", "HANDED_OFF", "INFO"})
def safe_code(value):
own = {"TELEMETRY_CONFIG", "TELEMETRY_READ", "TELEMETRY_PATH", "TELEMETRY_STORAGE", "TELEMETRY_LIMIT", "TELEMETRY_SCAN_PATH", "TELEMETRY_SCAN_IDENTITY", "TELEMETRY_SCAN_CHECKPOINT", "TELEMETRY_CLOCK", "TELEMETRY_EVENT", "TELEMETRY_SESSION", "TELEMETRY_OPERATION"}
return value if isinstance(value, str) and value in KNOWN_CODES | own else "UNCLASSIFIED_ERROR"
def attribute(value, name):
try:
return getattr(value, name, None)
except Exception:
return None
def error_code(error):
code = attribute(error, "code")
issues = attribute(attribute(error, "result"), "issues")
if code is None and isinstance(issues, (tuple, list)) and issues:
code = attribute(issues[0], "code")
return safe_code(code)
def diagnostic(error, max_frames):
"""No exception messages, paths, source lines, arguments or local values."""
result, seen = [], set()
while error is not None and id(error) not in seen and len(result) < 8:
seen.add(id(error))
module = type(error).__module__
trusted = module == "builtins" or module in KNOWN_MODULES and type(error).__name__ in KNOWN_CLASSES
frames = []
tb = attribute(error, "__traceback__")
while tb is not None and len(frames) < max_frames:
frame_module = tb.tb_frame.f_globals.get("__name__", "")
own = frame_module in KNOWN_MODULES
frames.append({"module": frame_module if own else "external", "function": tb.tb_frame.f_code.co_name if own else "external",
"line": tb.tb_lineno})
tb = tb.tb_next
os_errno = attribute(error, "errno")
keyword = attribute(error, "validator")
result.append({"type": type(error).__name__ if trusted else "ExternalError", "code": error_code(error),
"schema_keyword": keyword if isinstance(keyword, str) and keyword in {"type", "required", "additionalProperties", "oneOf", "anyOf", "const", "enum", "pattern", "minLength", "maxLength", "minItems", "maxItems", "minimum", "maximum"} else None,
"errno": os_errno if type(os_errno) is int and os_errno in errno.errorcode else None, "frames": frames})
# Suppressed context remains useful to operators without exposing its text.
error = attribute(error, "__cause__") or attribute(error, "__context__")
return result
STATES = frozenset({"NEW", "VALIDATED", "BUILDING_CASE", "INTERPRETING_CASE", "GENERATING_HYPOTHESES", "INVESTIGATING_HYPOTHESES", "BUILDING_DIAGNOSIS", "OUTPUT_READY", "HANDED_OFF", "BLOCKED", "FAILED_TO_ANALYZE", "REOPENED"})
CATEGORIES = frozenset({"ready", "blocked", "delayed", "human_wait", "manual_wait", "output_ready", "review", "handed_off"})
def hashed(value, length=64):
return value if isinstance(value, str) and re.fullmatch("[0-9a-f]{" + str(length) + "}", value) else None
def number(value):
return value if type(value) in {int, float} and math.isfinite(value) and value >= 0 else None
def http_status(value):
return value if type(value) is int and 100 <= value <= 599 else None
def detail(event, value):
"""Strict field allowlists even for callers emitting their own events."""
if not isinstance(value, dict):
return {}
if event == "telemetry_read_warning":
return {"corrupt_records": number(value.get("corrupt_records"))}
result = {}
if event == "operation_finished":
return {"duration_seconds": number(value.get("duration_seconds")), "reused": value.get("reused") is True,
"source_key": hashed(value.get("source_key")), "quarantine_key": hashed(value.get("quarantine_key"), 32)}
if event == "inventory_updated":
return {"complete": value.get("complete") is True, "inbox_files": number(value.get("inbox_files")),
"counts": {k: number(v) for k, v in value.get("counts", {}).items() if k in CATEGORIES}}
if event == "case_checkpoint":
return {"case_key": hashed(value.get("case_key")), "package_key": hashed(value.get("package_key")),
"state": value.get("state") if value.get("state") in STATES else None,
"package_generation": number(value.get("package_generation")), "analysis_revision": number(value.get("analysis_revision")),
"reason": safe_code(value.get("reason")) if value.get("reason") else None}
if event == "llm_round":
result = {"phase": value.get("phase") if value.get("phase") in STATES else None,
"http_status": http_status(value.get("http_status")),
"provider_key": hashed(value.get("provider_key")), "model_key": hashed(value.get("model_key")),
"input_tokens": number(value.get("input_tokens")), "output_tokens": number(value.get("output_tokens")),
"estimated_cost": number(value.get("estimated_cost")),
"currency": value.get("currency") if isinstance(value.get("currency"), str) and re.fullmatch("[A-Z]{3}", value["currency"]) else None,
"failed_providers": [{"provider_key": hashed(a.get("provider_key")), "code": safe_code(a.get("code")), "http_status": http_status(a.get("http_status"))} for a in value.get("failed_providers", [])[:64] if isinstance(a, dict)]}
from viano_bug_analysis.llm.diagnostics import sanitize
diagnostics = sanitize(value.get('diagnostics'))
if diagnostics:
result['diagnostics'] = diagnostics
if value.get('elapsed_seconds') is not None:
result['elapsed_seconds'] = number(value['elapsed_seconds'])
return result
if event in {"operation_failed", "phase_diagnostic"}:
chains = []
for error in value.get("exceptions", [])[:8]:
if not isinstance(error, dict):
continue
frames = []
for frame in error.get("frames", [])[:100]:
if isinstance(frame, dict):
module = frame.get("module") if frame.get("module") in KNOWN_MODULES else "external"
function = frame.get("function")
frames.append({"module": module, "function": function if module != "external" and isinstance(function, str) and re.fullmatch("[a-zA-Z_][a-zA-Z_0-9]{0,99}|<module>", function) else "external", "line": number(frame.get("line"))})
cls = error.get("type")
import builtins
chains.append({"type": cls if cls in KNOWN_CLASSES or isinstance(cls, str) and isinstance(getattr(builtins, cls, None), type) and issubclass(getattr(builtins, cls), BaseException) else "ExternalError",
"schema_keyword": error.get("schema_keyword") if error.get("schema_keyword") in {"type", "required", "additionalProperties", "oneOf", "anyOf", "const", "enum", "pattern", "minLength", "maxLength", "minItems", "maxItems", "minimum", "maximum"} else None,
"code": safe_code(error.get("code")), "errno": error.get("errno") if type(error.get("errno")) is int and error["errno"] in errno.errorcode else None, "frames": frames})
return {"exceptions": chains}
return result