informed-decision/informed_decision/runtime.py

193 lines
9.1 KiB
Python
Raw Normal View History

"""Owner-configured runtime and scheduled durable audit delivery.
This loads explicit configuration; it neither provisions credentials nor admits
a deployment. Projected caller tokens and Audit Core sender custody have owners.
"""
from dataclasses import dataclass
from datetime import datetime, timezone
import json
import os
from pathlib import Path
import stat
import tempfile
import threading
import time
from .approval_http import ApprovalHTTPClient, ordinary_review_ids
from .audit import AuditCoreSink, OutboxWorker
from .policy import PolicyClient
from .review import ReviewController
from .store import Store
def token_file(path):
path = Path(path)
if not path.is_absolute():
raise ValueError("credential file path must be absolute")
def read():
with path.open("rb") as handle:
value = handle.read(32769)
if len(value) > 32768:
raise ValueError("credential file too large")
return value.decode("ascii").strip()
return read
class AuditPump:
"""One process, bounded batches, 30s ticks; never drops a failed record."""
def __init__(self, store, sink, *, clock=time.time, intake_worker=None, lifecycle_delivery=False):
self.store, self.worker, self.clock = store, OutboxWorker(store, sink), clock
self.intake_worker = intake_worker
self.lifecycle_delivery = lifecycle_delivery
self._stop = threading.Event()
self._thread = None
self._lock = threading.Lock()
self._last_ok = None
self._last_reconciled = 0
def ready(self):
with self._lock:
recent = self._last_ok is not None and self.clock() - self._last_ok < 90
try:
return recent and self.store.lifecycle_counts()["pending"] == 0
except Exception:
return False
def tick(self):
try:
if self.intake_worker:
self.intake_worker.run_once(limit=10)
if self.lifecycle_delivery:
self.store.stage_lifecycle_audit()
self.store.queue_heartbeats(include_posed=self.intake_worker is not None,
include_lifecycle=self.lifecycle_delivery)
result = self.worker.run_once(limit=10)
if self.intake_worker:
self.store.settle_intake_audit()
if self.lifecycle_delivery:
self.store.settle_lifecycle_audit()
now = self.clock()
if now - self._last_reconciled >= 300:
stamp = lambda t: datetime.fromtimestamp(t, timezone.utc).isoformat(timespec="microseconds")
report = self.worker.reconcile(stamp(now - 86400), stamp(now))
# Report the two time bases, not an invented loss/completeness
# result. A retained private snapshot is for operator inspection.
directory = self.store.path.parent
fd, temporary = tempfile.mkstemp(prefix=".reconciliation-", dir=directory)
try:
with os.fdopen(fd, "w") as handle:
json.dump(report, handle, sort_keys=True)
handle.write("\n"); handle.flush(); os.fsync(handle.fileno())
os.replace(temporary, directory / "audit-reconciliation.json")
finally:
if os.path.exists(temporary):
os.unlink(temporary)
self._last_reconciled = now
healthy = not result["retrying"] and not result["blocked"] and not any(
row["state"] != "delivered" for row in self.store.outbox())
if self.intake_worker and self.store.intake_audit_pending():
healthy = False
if self.store.lifecycle_counts()["pending"]:
healthy = False
with self._lock:
self._last_ok = now if healthy else None
except Exception:
# Keep readiness closed and retry next tick. Never log response
# bodies, file paths, bearer tokens or a fabricated human decline.
with self._lock:
self._last_ok = None
def start(self):
if self._thread is not None:
raise RuntimeError("audit delivery already started")
def run():
while not self._stop.is_set():
self.tick()
self._stop.wait(30)
self._thread = threading.Thread(target=run, name="infd-audit-delivery", daemon=True)
self._thread.start()
def stop(self):
self._stop.set()
if self._thread is not None:
self._thread.join(timeout=6)
@dataclass(frozen=True)
class Runtime:
controller: ReviewController
pump: AuditPump
lifecycle: object | None = None
workload: object | None = None
@classmethod
def from_file(cls, filename):
path = Path(filename)
info = path.lstat()
if (not path.is_absolute() or not stat.S_ISREG(info.st_mode) or info.st_nlink != 1
or info.st_uid != os.getuid() or stat.S_IMODE(info.st_mode) != 0o600
or info.st_size > 16384):
raise ValueError("runtime configuration must be an owned private 0600 file")
data = json.loads(path.read_text())
if ((set(data) - {"ordinary_approval_ids", "intake_delivery", "lifecycle_policy", "lifecycle_delivery", "workload_api", "intake_attributes"}) != {"schema", "evidence_db", "policy", "approval_origin", "audit"}
or data["schema"] != "informed-decision.review-runtime.v1"
or set(data["policy"]) != {"origin", "package", "version", "package_digest", "caller_token_file"}
or set(data["audit"]) != {"origin", "sender_token_file"}):
raise ValueError("invalid review runtime configuration")
lifecycle_delivery = data.get("lifecycle_delivery", False)
if type(lifecycle_delivery) is not bool:
raise ValueError("invalid lifecycle delivery setting")
intake_attributes = data.get('intake_attributes', False)
if type(intake_attributes) is not bool:
raise ValueError('invalid intake attributes setting')
ordinary = ordinary_review_ids(data.get("ordinary_approval_ids", []))
if not Path(data["evidence_db"]).is_absolute():
raise ValueError("absolute evidence database path required")
from .http_transport import fixed_origin
approval_origin = fixed_origin(data["approval_origin"], allow_internal_http=True)
policy = data["policy"]
client = PolicyClient(policy["origin"], token_file(policy["caller_token_file"]),
package=policy["package"], version=policy["version"], package_digest=policy["package_digest"],
allow_internal_http=True)
sink = AuditCoreSink(data["audit"]["origin"], token_file(data["audit"]["sender_token_file"]),
allow_internal_http=True)
delivery = data.get('intake_delivery')
hub = None
if 'intake_delivery' in data:
from .intake_hub import HubDelivery
if not isinstance(delivery, dict) or set(delivery) != {'hub_origin'}:
raise ValueError('invalid intake delivery configuration')
hub = HubDelivery(delivery['hub_origin'])
lifecycle_policy = None
if "lifecycle_policy" in data:
configuration = data["lifecycle_policy"]
if (not isinstance(configuration, dict)
or set(configuration) != {"origin", "package", "version", "package_digest", "caller_token_file"}):
raise ValueError("invalid lifecycle policy configuration")
if configuration["package"] == policy["package"]:
raise ValueError("lifecycle policy must have a separate admission package")
lifecycle_policy = PolicyClient(configuration["origin"], token_file(configuration["caller_token_file"]),
package=configuration["package"], version=configuration["version"],
package_digest=configuration["package_digest"], allow_internal_http=True)
store = Store(data["evidence_db"])
controller = ReviewController(store, client, lambda session: ApprovalHTTPClient(
approval_origin, session, allow_internal_http=True, ordinary_approval_ids=tuple(ordinary)),
intake_attributes=intake_attributes)
worker = None
if hub:
from .intake_delivery import IntakeDeliveryWorker
worker = IntakeDeliveryWorker(store, audit=sink, notify=hub.notify,
hub_transport=hub.resolution, hub_base_url=hub.origin)
lifecycle = None
if lifecycle_policy is not None:
from .lifecycle import LifecycleRepository
from .lifecycle_service import LifecycleService
lifecycle = LifecycleService(LifecycleRepository(store), lifecycle_policy)
workload = None
if 'workload_api' in data:
from .workload import compose_workload
workload = compose_workload(data['workload_api'], store, lifecycle, approval_origin, token_file)
return cls(controller, AuditPump(store, sink, intake_worker=worker,
lifecycle_delivery=lifecycle_delivery), lifecycle, workload)