informed-decision/informed_decision/runtime.py

131 lines
5.5 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
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):
self.store, self.worker, self.clock = store, OutboxWorker(store, sink), clock
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:
return self._last_ok is not None and self.clock() - self._last_ok < 90
def tick(self):
try:
self.store.queue_heartbeats()
result = self.worker.run_once(limit=10)
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())
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
@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) != {"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")
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)
store = Store(data["evidence_db"])
controller = ReviewController(store, client, lambda session: ApprovalHTTPClient(
approval_origin, session, allow_internal_http=True))
return cls(controller, AuditPump(store, sink))