"""Fail-closed privacy boundary for model-bound fact packages.""" from __future__ import annotations import re from collections.abc import Iterable, Mapping from dataclasses import dataclass from typing import Any class UnsafeFactPackage(ValueError): """Raised before a model call when a package may contain sensitive data.""" _DENIED_KEYS = { "name", "display_name", "email", "mail", "phone", "mobile", "contact", "ip", "ip_address", "public_ip", "private_ip", "hostname", "machine_name", "domain", "domain_name", "account", "account_name", "username", "login", "secret", "password", "token", "api_key", "access_key", "private_key", "policy", "raw_policy", "raw_data", "raw", "audit_text", "description", } _DENIED_KEY_PARTS = ("password", "secret", "token", "private_key", "access_key") _EMAIL = re.compile(r"(?i)\b[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9-]+(?:\.[a-z0-9-]+)+\b") _IPV4 = re.compile(r"(? tuple[list[str], dict[str, Any]]: """Assign stable-in-package anonymous references to local objects.""" counters: dict[str, int] = {} refs: list[str] = [] mapping: dict[str, Any] = {} seen: set[tuple[str, str]] = set() # Sorting makes the same domain facts produce the same references/hash. for obj in sorted(objects, key=lambda item: (item.kind, item.object_id)): identity = (obj.kind, obj.object_id) if identity in seen: continue seen.add(identity) kind = re.sub(r"[^a-z0-9]", "", obj.kind.lower())[:12] or "obj" counters[kind] = counters.get(kind, 0) + 1 ref = f"{kind}_{counters[kind]:02d}" refs.append(ref) mapping[ref] = { "kind": obj.kind, "object_id": obj.object_id, } return refs, mapping def merge_anonymous_fact_packages( packages: Mapping[str, tuple[Any, Mapping[str, Any]]], ) -> tuple[dict[str, Any], dict[str, Any]]: """Merge independently numbered packages into one collision-free namespace.""" objects: list[EvidenceObject] = [] for payload, reference_map in packages.values(): validate_anonymous_refs(payload, set(reference_map)) for ref, value in reference_map.items(): if ( not isinstance(ref, str) or not isinstance(value, Mapping) or set(value) != {"kind", "object_id"} or not all( isinstance(value.get(key), str) and value[key] for key in ("kind", "object_id") ) ): raise UnsafeFactPackage("Fact package contains an invalid reference map") objects.append(EvidenceObject(value["kind"], value["object_id"])) _, merged_map = build_reference_map(objects) reverse = { (value["kind"], value["object_id"]): ref for ref, value in merged_map.items() } def replace(value: Any, replacements: Mapping[str, str]) -> Any: if isinstance(value, dict): return {key: replace(child, replacements) for key, child in value.items()} if isinstance(value, list): return [replace(child, replacements) for child in value] if isinstance(value, tuple): return tuple(replace(child, replacements) for child in value) if isinstance(value, str): return replacements.get(value, value) return value merged_payloads: dict[str, Any] = {} for name, (payload, reference_map) in packages.items(): replacements = { local_ref: reverse[(value["kind"], value["object_id"])] for local_ref, value in reference_map.items() } merged_payloads[name] = replace(payload, replacements) assert_llm_safe(merged_payloads) validate_anonymous_refs(merged_payloads, set(merged_map)) return merged_payloads, merged_map def _sensitive_shape(candidate: str) -> str | None: """Name the recognizable sensitive shape in a string, if any.""" if _EMAIL.search(candidate): return "email-like value" if _IPV4.search(candidate): return "IP-like value" if _DOMAIN.search(candidate): return "domain-like value" if _SECRET.search(candidate): return "credential-like value" return None def _key_shape(key: str, *, flat_path: bool) -> str | None: """Name the sensitive shape in a KEY, reading flat keys as addresses. A flattened key is an address our own flattener joined — ``metrics`` + ``cloud.access.governed_coverage`` + ``value``. Handing that joined string to the value regexes reads our vocabulary as a hostname: ``metrics.cloud`` is ``