Stdlib per-tenant service: trusted-header identity (router/relay/worker, constant-time keys, slot pinned to the pod), fail-closed card flags with capability negotiation, tenant-scoped store (atomic writes, revisions, append-only ledgers, content-addressed blobs, manifest), audit outcome for every request, and the /hux/v1 pipeline that maps errors to hux.error.v1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RNPhwu2bsaRNg3DETSAZoM
41 lines
1.3 KiB
Python
41 lines
1.3 KiB
Python
"""Auditable outcome for every read and mutation.
|
|
|
|
One JSONL ledger per tenant per day. The record shape is
|
|
``common.schema.json#/$defs/audit_outcome``; it never carries request bodies,
|
|
only the action name, the resource id and the decision.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from hux.identity import Identity
|
|
from hux.store import TenantStore, now_iso
|
|
|
|
OUTCOMES = ("allow", "deny", "not_found", "conflict", "flag_off")
|
|
|
|
|
|
def record(store: TenantStore, identity: Identity, action: str, resource: str, outcome: str, reason: str = "") -> dict:
|
|
"""Append an audit outcome and return it."""
|
|
if outcome not in OUTCOMES:
|
|
raise ValueError(f"unknown outcome {outcome!r}")
|
|
entry = {
|
|
"at": now_iso(),
|
|
"identity": identity.record(),
|
|
"action": action,
|
|
"resource": resource[:200],
|
|
"outcome": outcome,
|
|
}
|
|
if reason:
|
|
entry["reason"] = reason[:200]
|
|
store.append("audit", now_iso()[:10], entry)
|
|
return entry
|
|
|
|
|
|
def recent(store: TenantStore, limit: int = 200) -> list[dict]:
|
|
"""Newest audit rows across day ledgers, newest last."""
|
|
rows: list[dict] = []
|
|
for name in reversed(store.ledgers("audit")):
|
|
rows = store.read("audit", name) + rows
|
|
if len(rows) >= limit:
|
|
break
|
|
return rows[-limit:]
|