Connect policy-gated browser review and audit runtime
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
parent
2cc32168ac
commit
83849b75d4
35 changed files with 2381 additions and 83 deletions
130
informed_decision/runtime.py
Normal file
130
informed_decision/runtime.py
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
"""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))
|
||||
Loading…
Add table
Add a link
Reference in a new issue