feat: tamper-evident audit ledger (hash chain) + dual-column cost view
- Hash-chain ledger (cloud/ledger.py): prev_hash/entry_hash per record, SQLite append-only triggers, UNIQUE prev/entry hash with fail-closed retry at the transaction boundary; verify/export (incl. expected-head check for tail truncation); legacy audit_log rows marked pre-chain via a single ledger_genesis marker, never rewritten. - AuthStore.audit() writes audit_log + chain entry in one transaction. - Track-3 interface: audit_verdict / audit_pushback_round / audit_rebuild / third-failure assessment / test-change audit / meta-review / rebuild-notice entries; per-(module, contract-version) failure counts derived from the ledger (pass or completed rebuild resets; total > 3 triggers rebuild); acceptance-triggered rebuilds require a confirmed user notice. - Admin read-only endpoints: GET /admin/audit-ledger/verify|export. - Cost: dual_column_view (priced_usd_micros + billing note, wording reused verbatim from PR #212) wired into project/task cost endpoints; by-model and per-creator reporting stay in #212, not duplicated here. - PG schema + migration lists updated for audit_ledger (incl. sequence setval alignment).
This commit is contained in:
1 parent
e26624b1bc
commit
04a7a4280b
7 files changed
+2123
-9
No files matched your search
@@ -75,8 +75,13 @@ from cloud.cost import CostStore, unpriced_token_cap
|
||||
from cloud.models import evaluate_capability as _evaluate_capability
|
||||
from cloud.models import load_routing_policy as _load_routing_policy
|
||||
from cloud.routing_shadow import evaluate_shadow as _evaluate_shadow
|
||||
# 演练注:本提交原 diff 曾删 CostStore 导入(其树内仍有 5 处
|
||||
# CostStore 调用,属源线中间态潜 bug,后续源提交又拆行补回);
|
||||
# 此处取并集保留导入,行为与源线终态一致。
|
||||
from cloud.gates import merge_state_decision as _merge_state_decision
|
||||
from cloud.gates import resolve_inside as _gate_resolve_inside
|
||||
from cloud.ledger import export_chain as _ledger_export
|
||||
from cloud.ledger import verify_chain as _ledger_verify
|
||||
from cloud.quota import decide as _quota_decide
|
||||
from cloud.quota import default_quota_usd_micros as _quota_default
|
||||
from cloud.quota import monthly_usage as _quota_monthly_usage
|
||||
@@ -3166,6 +3171,10 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
|
||||
self._admin_quota_get(user_id, query)
|
||||
elif path == "/admin/quotas" and method == "POST":
|
||||
self._admin_quota_set(user_id)
|
||||
elif path == "/admin/audit-ledger/verify" and method == "GET":
|
||||
self._admin_ledger_verify(user_id)
|
||||
elif path == "/admin/audit-ledger/export" and method == "GET":
|
||||
self._admin_ledger_export(user_id, query)
|
||||
elif path == "/admin/push/dispatch" and method == "POST":
|
||||
self._push_dispatch_endpoint(user_id)
|
||||
elif path.startswith("/tasks/"):
|
||||
@@ -9486,6 +9495,54 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
|
||||
target, quota, used, month,
|
||||
self._quota_unpriced_tokens(target)))
|
||||
|
||||
def _admin_ledger_verify(self, user_id):
|
||||
"""GET /admin/audit-ledger/verify:全局管理员只读校验账本。
|
||||
|
||||
只读:成功路径不写审计(写审计会改链头,校验读数本身
|
||||
就不再可复现);被拒才记 admin_ledger_denied。校验失败
|
||||
仍回 200,ok=false + failures 供运维判读,不是 HTTP 错。
|
||||
"""
|
||||
if not self._is_global_admin(user_id):
|
||||
self._audit_admin(user_id, "admin_ledger_denied",
|
||||
"verify ledger")
|
||||
self._send_json(403, {"error": "forbidden"})
|
||||
return
|
||||
conn = self._db()
|
||||
try:
|
||||
result = _ledger_verify(conn)
|
||||
finally:
|
||||
conn.close()
|
||||
self._send_json(200, result)
|
||||
|
||||
def _admin_ledger_export(self, user_id, query):
|
||||
"""GET /admin/audit-ledger/export:全局管理员只读导出账本。
|
||||
|
||||
?limit=N 可截断(运维取头锚点用);limit 非法回 400。
|
||||
导出含头哈希,运维应把头哈希存到库外作比对锚点。
|
||||
"""
|
||||
if not self._is_global_admin(user_id):
|
||||
self._audit_admin(user_id, "admin_ledger_denied",
|
||||
"export ledger")
|
||||
self._send_json(403, {"error": "forbidden"})
|
||||
return
|
||||
limit_raw = (query.get("limit") or [None])[0]
|
||||
limit = None
|
||||
if limit_raw is not None:
|
||||
try:
|
||||
limit = int(limit_raw)
|
||||
except (TypeError, ValueError):
|
||||
limit = -1
|
||||
if limit < 0:
|
||||
self._send_json(
|
||||
400, {"error": "limit must be a non-negative integer"})
|
||||
return
|
||||
conn = self._db()
|
||||
try:
|
||||
result = _ledger_export(conn, limit=limit)
|
||||
finally:
|
||||
conn.close()
|
||||
self._send_json(200, result)
|
||||
|
||||
def _admin_quota_set(self, user_id):
|
||||
if not self._is_global_admin(user_id):
|
||||
self._audit_admin(user_id, "admin_quota_denied",
|
||||
|
||||
Reference in new issue
Block a user