from __future__ import annotations import hashlib import json from dataclasses import dataclass from pathlib import Path from typing import Any from jsonschema import validate from app.file_pipeline import DetectorManifest from app.rules import RuleBundle @dataclass(frozen=True) class ActiveConfig: version: int document: dict[str, Any] rules: RuleBundle detector: DetectorManifest @property def rules_version(self) -> str: return self.rules.version def canonical_config(document: dict[str, Any]) -> bytes: return json.dumps(document, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode() def validate_config( document: dict[str, Any], artifacts: Path ) -> tuple[RuleBundle, DetectorManifest, bytes]: schema = json.loads((artifacts / "config.schema.json").read_text(encoding="utf-8")) validate(document, schema) task = document["task"] if not task["heartbeat_sec"] < task["lease_sec"] < task["execution_deadline_sec"]: raise ValueError("heartbeat_sec < lease_sec < execution_deadline_sec is required") rules_ref = document["rules_bundle_ref"] rules = RuleBundle.load( artifacts / "rules" / rules_ref / "rules.yaml", artifacts / "rules" / "rules.schema.json", ) detector = DetectorManifest.load(artifacts / "detector-manifest.json") enabled = set(document["file_policy"]["enabled_mime_types"]) if not enabled <= detector.supported: raise ValueError("file policy is not a detector manifest subset") if document["file_policy"]["max_size_bytes"] > detector.max_size: raise ValueError("file policy exceeds detector hard limit") digest = hashlib.sha256(canonical_config(document)).digest() return rules, detector, digest