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:
host committed 2026-11-03 02:49:25 +00:00
1 parent c0ab995ccc
commit eeb10118d6
7 files changed
+2121 -10

No files matched your search

+37 -7
View File
@@ -24,6 +24,8 @@ approve_merge -> maintainer,manage_project -> owner(仅 owner)。
import hashlib
import secrets
from cloud.db import connect as _db_connect
from cloud.ledger import append_entry as _ledger_append
from cloud.ledger import ensure_schema as _ledger_ensure
import time
import uuid
@@ -68,6 +70,9 @@ class AuthStore:
"detail TEXT, "
"created_at REAL)"
)
# 防篡改账本(哈希链):audit_log 本身不动,链写进
# audit_ledger;建表失败即建库失败,不静默丢链。
_ledger_ensure(self.conn)
self.conn.commit()
def create_user(self, username):
@@ -349,13 +354,38 @@ class AuthStore:
)
def audit(self, user_id, project_id, action, detail=""):
"""只追加一条审计记录。"""
self.conn.execute(
"INSERT INTO audit_log(user_id, project_id, action, detail, created_at)"
" VALUES(?, ?, ?, ?, ?)",
(user_id, project_id, action, detail, time.time()),
)
self.conn.commit()
"""只追加一条审计记录,并同步写哈希链(同事务)。
audit_log 行与 audit_ledger 链条目同 created_at、同事务
提交:链写失败时整对回滚并抛错(fail-closed,绝不留
“audit_log 有行、链上无行”的静默缺口)。并发抢链头
导致唯一冲突时在事务边界重试整对写入,最多 5 次;
其它错误直接回滚抛错,不重试。
"""
now = time.time()
last_exc = None
for _attempt in range(5):
try:
self.conn.execute(
"INSERT INTO audit_log(user_id, project_id, action,"
" detail, created_at)"
" VALUES(?, ?, ?, ?, ?)",
(user_id, project_id, action, detail, now),
)
_ledger_append(self.conn, user_id, project_id, action,
detail=detail, created_at=now)
self.conn.commit()
return
except Exception as exc:
last_exc = exc
try:
self.conn.rollback()
except Exception:
pass
if not isinstance(exc, sqlite3.IntegrityError) \
and type(exc).__name__ != "PostgresIntegrityError":
raise
raise last_exc
def list_audit(self, project_id, limit=100):
"""按 created_at 倒序返回该项目的审计列表(dict)。"""
+20
View File
@@ -135,6 +135,8 @@ def price_usd_micros(model, input_tokens, cached_input_tokens,
return int(round(usd * 1000000))
# 双列文案:逐字复用 PR #212(feat-cost-quota,5b30c61)的
# BILLING_NOTE,本轨不拥有成本口径、不另造一套说法。
BILLING_NOTE = (
"价格表代理价(经 CLOUD_MODEL_PRICES_JSON / CLOUD_MODEL_PRICES_FILE"
" 覆盖层),非中转实际账单;未定价 token 不计入美元、不等于免费。"
@@ -168,6 +170,24 @@ def quota_cutoff_reason(quota_usd_micros, used_usd_micros,
return None
def dual_column_view(report):
"""双列视图:在已有 _report 结果上加定价列别名与账单说明。
不改 _report(旧单测按其键集断言)与旧键:usd_micros 仍是
有价行之和,unpriced_actual_tokens 仍是未定价 actual token;
本函数只加 priced_usd_micros(与 usd_micros 同值)与
billing_note。未定价模型分列(by_model / unpriced_models)
与按创建者口径是 PR #212 的范围,本轨不重复实现——#212
合并后本函数由其 dual_column_* 取代,端点改走 dual_column_*
(同名同值键,切换无语义差);#212 未合并前,本视图是
任务/项目成本端点的双列最小实现。
"""
view = dict(report)
view["priced_usd_micros"] = view["usd_micros"]
view["billing_note"] = BILLING_NOTE
return view
def unpriced_token_cap():
"""Monthly unpriced-token ceiling for users who have a quota row.
+15 -1
View File
@@ -426,7 +426,8 @@ _OR_REPLACE_PK = {
}
_IDENTITY_TABLES = frozenset(
{"summaries", "kb_docs", "cost_records", "audit_log"})
{"summaries", "kb_docs", "cost_records", "audit_log",
"audit_ledger"})
_OR_REPLACE_RE = _re.compile(
r"^\s*INSERT\s+OR\s+REPLACE\s+INTO\s+(\w+)\s*\(([^)]+)\)\s*"
@@ -804,6 +805,19 @@ POSTGRES_SCHEMA = (
"action TEXT, "
"detail TEXT, "
"created_at DOUBLE PRECISION)",
# 防篡改账本(哈希链,第 4 轨):与 cloud/ledger.py 的 SQLite
# DDL 同表同列;prev/entry_hash UNIQUE 是并发抢链头的
# fail-closed 基础,绝不能去掉。
"CREATE TABLE IF NOT EXISTS audit_ledger("
"id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, "
"user_id TEXT, "
"project_id TEXT, "
"task_id TEXT, "
"action TEXT, "
"detail TEXT, "
"created_at DOUBLE PRECISION, "
"prev_hash TEXT NOT NULL UNIQUE, "
"entry_hash TEXT NOT NULL UNIQUE)",
# P0 三件:断点与步骤事实源(PG 新库一次建齐;老库走
# server/worker 的 ensure 幂等补表,不丢数据)。
# task_checkpoints:每轮断点快照(worker 镜像线程 + 终局
+1393
View File
File diff suppressed because it is too large. Load diff
+5 -1
View File
@@ -46,6 +46,9 @@ MIGRATION_TABLES = (
"project_members",
"tasks",
"audit_log",
# 防篡改账本(哈希链,第 4 轨):与 audit_log 同款身份列,
# 迁库时整行原样迁(id 与哈希不动,链不断),序列见下。
"audit_ledger",
"summaries",
"kb_docs",
"cost_records",
@@ -91,12 +94,13 @@ MIGRATION_TABLES = (
_SKIP_TABLES = frozenset({"kb_fts", "sqlite_sequence"})
# 身份列的表:POSTGRES_SCHEMA 里唯一用
# ``BIGINT GENERATED BY DEFAULT AS IDENTITY`` 的 4 张表。
# ``BIGINT GENERATED BY DEFAULT AS IDENTITY`` 的 5 张表。
# 迁移时显式插 id 不会推序列,迁完必须 setval 到 MAX(id),
# 否则后续省略 id 的 INSERT 会撞主键。新增身份列表必须
# 同步改这里,test_p2_migrations.py 会拿 schema 实测对齐。
SEQUENCE_TABLES = (
"audit_log",
"audit_ledger",
"summaries",
"kb_docs",
"cost_records",
+55 -1
View File
@@ -63,9 +63,11 @@ from cloud.auth import ROLES as _MEMBER_ROLES
from cloud.auth import AuthStore
from cloud.coordinator import parse_parts as _coord_parse_parts
from cloud.coordinator import validate_parts as _coord_validate_parts
from cloud.cost import CostStore, unpriced_token_cap
from cloud.cost import unpriced_token_cap
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
@@ -3075,6 +3077,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/"):
@@ -8997,6 +9003,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",
+596
View File
@@ -0,0 +1,596 @@
"""防篡改账本(哈希链)+ 成本双列视图单测。
不联网、不跑真实模型:账本走临时 SQLite 文件,篡改/删除
用 SQL 直写模拟(先 DROP 触发器,模拟有 DDL 权限的攻击者;
触发器本身另有单测),服务端点走本地临时端口。
"""
import http.client
import json
import os
import sqlite3
import sys
import tempfile
import threading
import unittest
from pathlib import Path
try:
from cloud.auth import AuthStore
from cloud.cost import CostStore, dual_column_view, price_usd_micros
from cloud.ledger import (
GENESIS_HASH, LedgerStore, append_pushback_round,
audit_timeline, export_chain, list_pushback_rounds,
module_failure_counts, verify_chain, verify_export)
from cloud.server import create_server, init_db
except ImportError: # discover -s cloud/tests 时把项目根目录补进 sys.path
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from cloud.auth import AuthStore
from cloud.cost import CostStore, dual_column_view, price_usd_micros
from cloud.ledger import (
GENESIS_HASH, LedgerStore, append_pushback_round,
audit_timeline, export_chain, list_pushback_rounds,
module_failure_counts, verify_chain, verify_export)
from cloud.server import create_server, init_db
def _req(port, method, path, key=None):
conn = http.client.HTTPConnection("127.0.0.1", port, timeout=10)
hdrs = {}
if key is not None:
hdrs["X-API-Key"] = key
try:
conn.request(method, path, headers=hdrs)
resp = conn.getresponse()
data = resp.read()
finally:
conn.close()
try:
parsed = json.loads(data.decode("utf-8")) if data else None
except ValueError:
parsed = None
return resp.status, parsed
class TestLedgerChain(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.store = LedgerStore(os.path.join(self.tmp.name, "led.db"))
def tearDown(self):
self.store.close()
self.tmp.cleanup()
def test_append_and_verify_ok(self):
self.store.append("u1", "p1", "create_task", "t1")
self.store.append("u1", "p1", "approve_merge", "t1")
result = self.store.verify()
self.assertTrue(result["ok"])
self.assertEqual(result["count"], 2)
self.assertEqual(result["failures"], [])
self.assertNotEqual(result["head_hash"], GENESIS_HASH)
def test_append_only_interface(self):
# 接口层面即只追加:没有修改/删除方法(test_auth 同款断言)。
self.assertFalse(hasattr(LedgerStore, "delete_entry"))
self.assertFalse(hasattr(LedgerStore, "update_entry"))
self.assertFalse(hasattr(LedgerStore, "delete"))
self.assertFalse(hasattr(LedgerStore, "update"))
def test_sqlite_triggers_block_update_and_delete(self):
self.store.append("u1", "p1", "create_task", "t1")
with self.assertRaises(Exception):
self.store.conn.execute(
"UPDATE audit_ledger SET detail='x' WHERE id=1")
with self.assertRaises(Exception):
self.store.conn.execute(
"DELETE FROM audit_ledger WHERE id=1")
self.store.conn.rollback()
self.assertTrue(self.store.verify()["ok"])
def test_tamper_detail_fails_verify(self):
self.store.append("u1", "p1", "create_task", "t1")
self.store.append("u1", "p1", "approve_merge", "t1")
# 模拟有 DDL 权限的攻击者:先卸触发器再改历史。
self.store.conn.execute(
"DROP TRIGGER IF EXISTS audit_ledger_no_update")
self.store.conn.execute(
"UPDATE audit_ledger SET detail='forged' WHERE id=1")
self.store.conn.commit()
result = self.store.verify()
self.assertFalse(result["ok"])
self.assertTrue(any("tampered" in f for f in result["failures"]))
def test_delete_middle_breaks_chain(self):
for i in range(3):
self.store.append("u1", "p1", "act-%d" % i, "d%d" % i)
self.store.conn.execute(
"DROP TRIGGER IF EXISTS audit_ledger_no_delete")
self.store.conn.execute("DELETE FROM audit_ledger WHERE id=2")
self.store.conn.commit()
result = self.store.verify()
self.assertFalse(result["ok"])
self.assertTrue(any("broken" in f for f in result["failures"]))
def test_delete_tail_detected_by_old_head(self):
for i in range(3):
self.store.append("u1", "p1", "act-%d" % i, "d%d" % i)
old_export = self.store.export()
old_head = old_export["head_hash"]
self.store.conn.execute(
"DROP TRIGGER IF EXISTS audit_ledger_no_delete")
self.store.conn.execute(
"DELETE FROM audit_ledger WHERE id=3")
self.store.conn.commit()
# 截断后的链自洽(固有局限,如实断言),但旧头对不上。
self.assertTrue(self.store.verify()["ok"])
self.assertFalse(
self.store.verify(expected_head=old_head)["ok"])
self.assertFalse(
verify_export(self.store.export(),
expected_head=old_head)["ok"])
def test_export_round_trip_and_tampered_export(self):
self.store.append("u1", "p1", "create_task", "t1")
self.store.append("u1", "p1", "approve_merge", "t1")
data = self.store.export()
self.assertTrue(verify_export(data)["ok"])
self.assertEqual(data["count"], 2)
data["entries"][0]["detail"] = "forged"
self.assertFalse(verify_export(data)["ok"])
data2 = self.store.export()
data2["head_hash"] = "0" * 64
self.assertFalse(verify_export(data2)["ok"])
def test_legacy_migration_marks_pre_chain_without_rewrite(self):
# 老 audit_log 行(直接 INSERT,模拟链上线前的历史)。
conn = sqlite3.connect(os.path.join(self.tmp.name, "led.db"))
try:
conn.execute(
"CREATE TABLE IF NOT EXISTS audit_log("
"id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT,"
" project_id TEXT, action TEXT, detail TEXT,"
" created_at REAL)")
conn.execute(
"INSERT INTO audit_log(user_id, project_id, action,"
" detail, created_at) VALUES(?,?,?,?,?)",
("u1", "p1", "create_task", "t0", 1000.0))
conn.execute(
"INSERT INTO audit_log(user_id, project_id, action,"
" detail, created_at) VALUES(?,?,?,?,?)",
("u1", "p1", "approve_merge", "t0", 1001.0))
conn.commit()
finally:
conn.close()
# 链已空但老表有行:verify 必须 fail-closed,不认账。
fresh = LedgerStore(os.path.join(self.tmp.name, "led.db"))
try:
result = fresh.verify()
self.assertFalse(result["ok"])
self.assertTrue(any("legacy_not_migrated" in f
for f in result["failures"]))
migrated = fresh.migrate_legacy()
self.assertTrue(migrated["migrated"])
self.assertEqual(
migrated["marker"]["action"], "ledger_genesis")
after = fresh.verify()
self.assertTrue(after["ok"])
self.assertIsNotNone(after["genesis_marker"])
self.assertEqual(
after["genesis_marker"]["legacy_audit_rows"], 2)
# 迁移后链已启动,再跑迁移必须拒绝,不补不改。
self.assertFalse(fresh.migrate_legacy()["migrated"])
# 老行原样未动:仍 2 行、无哈希列。
raw = sqlite3.connect(os.path.join(self.tmp.name, "led.db"))
try:
rows = raw.execute(
"SELECT COUNT(*) FROM audit_log").fetchone()
cols = [c[1] for c in raw.execute(
"PRAGMA table_info(audit_log)").fetchall()]
finally:
raw.close()
self.assertEqual(rows[0], 2)
self.assertNotIn("entry_hash", cols)
finally:
fresh.close()
def test_auth_audit_writes_chain_same_transaction(self):
db_path = os.path.join(self.tmp.name, "auth.db")
auth = AuthStore(db_path)
try:
owner_id, _key = auth.create_user("owner")
auth.create_project("p1", owner_id)
auth.audit(owner_id, "p1", "create_task", "task-1")
auth.audit(owner_id, "p1", "approve_merge", "task-1")
self.assertEqual(len(auth.list_audit("p1")), 2)
finally:
auth.conn.close()
led = LedgerStore(db_path)
try:
result = led.verify()
self.assertTrue(result["ok"])
self.assertEqual(result["count"], 2)
actions = [e["action"] for e in
led.export()["entries"]]
self.assertEqual(actions, ["create_task", "approve_merge"])
finally:
led.close()
class TestPushbackAndRebuild(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.store = LedgerStore(os.path.join(self.tmp.name, "led.db"))
def tearDown(self):
self.store.close()
self.tmp.cleanup()
def _verdict(self, verdict, source, task="task-1"):
return self.store.append_audit_verdict(
task, "p1", "mod-a", "v1", verdict, source,
failures=["f1"] if verdict == "fail" else [])
def test_failure_counts_split_sources_and_reset_on_pass(self):
self._verdict("fail", "audit")
self._verdict("fail", "acceptance")
counts = self.store.module_failure_counts("mod-a", "v1")
self.assertEqual(counts["audit_fails"], 1)
self.assertEqual(counts["acceptance_fails"], 1)
self.assertEqual(counts["total_fails"], 2)
self.assertFalse(counts["rebuild_triggered"])
self._verdict("pass", "audit")
counts = self.store.module_failure_counts("mod-a", "v1")
self.assertEqual(counts["total_fails"], 0)
def test_contract_version_change_restarts_count(self):
self._verdict("fail", "audit")
self._verdict("fail", "audit")
self.assertEqual(
self.store.module_failure_counts("mod-a", "v1")["total_fails"],
2)
self.assertEqual(
self.store.module_failure_counts("mod-a", "v2")["total_fails"],
0)
def test_rebuild_trigger_and_rebuild_resets_baseline(self):
for source in ("audit", "acceptance", "audit"):
self._verdict("fail", source)
counts = self.store.module_failure_counts("mod-a", "v1")
self.assertEqual(counts["total_fails"], 3)
self.assertFalse(counts["rebuild_triggered"])
self._verdict("fail", "audit") # 第 4 次:推倒触发点
counts = self.store.module_failure_counts("mod-a", "v1")
self.assertTrue(counts["rebuild_triggered"])
self.store.append_rebuild_decision(
"task-1", "p1", "mod-a", "v1", 1,
{"commit": "abc123"}, root_cause="implementation",
status="decided", prior_failure_total=4)
with self.assertRaises(ValueError):
# rebuilt 必须带新实现引用。
self.store.append_rebuild_decision(
"task-1", "p1", "mod-a", "v1", 1,
{"commit": "abc123"}, root_cause="implementation",
status="rebuilt")
self.store.append_rebuild_decision(
"task-1", "p1", "mod-a", "v1", 1,
{"commit": "abc123"}, new_impl_ref={"commit": "def456"},
root_cause="implementation", status="rebuilt",
prior_failure_total=4)
counts = self.store.module_failure_counts("mod-a", "v1")
self.assertEqual(counts["total_fails"], 0)
# 重做后再超 3 次:同样触发(不再自动重开是升级语义,
# 账本只负责计数与触发信号,不替第 3 轨做升级决策)。
for _ in range(4):
self._verdict("fail", "audit")
self.assertTrue(
self.store.module_failure_counts(
"mod-a", "v1")["rebuild_triggered"])
def test_contract_design_root_cause_recorded_not_rebuild(self):
entry = self.store.append_rebuild_decision(
"task-1", "p1", "mod-a", "v1", 1,
{"tag": "v1-old"}, root_cause="contract_design",
status="decided")
self.assertEqual(entry["action"], "audit_rebuild")
with self.assertRaises(ValueError):
self.store.append_rebuild_decision(
"task-1", "p1", "mod-a", "v1", 2,
{"commit": "x"}, root_cause="something_else")
def test_timeline_reconstructs_full_path_without_erasure(self):
# fail → 推回 1-3 轮 → 第 4 次 fail → 推倒 → 重做 → 再审 pass
self._verdict("fail", "audit")
for round_no in (1, 2, 3):
self.store.append_pushback_round(
"task-1", "p1", round_no, ["f1"],
{"failures": ["f1"]},
module_id="mod-a", contract_version="v1",
failure_source="audit")
self._verdict("fail", "audit")
# 此时累计:首 fail + 3 轮后各 1 fail = 4 次,已触发推倒。
self.assertTrue(
self.store.module_failure_counts(
"mod-a", "v1")["rebuild_triggered"])
self.store.append_rebuild_decision(
"task-1", "p1", "mod-a", "v1", 1,
{"commit": "old"}, new_impl_ref={"commit": "new"},
root_cause="implementation", status="rebuilt",
prior_failure_total=4)
self._verdict("pass", "audit")
timeline = self.store.audit_timeline(task_id="task-1")
actions = [e["action"] for e in timeline["entries"]]
self.assertEqual(actions.count("audit_verdict"), 5)
self.assertEqual(actions.count("audit_pushback_round"), 3)
self.assertEqual(actions.count("audit_rebuild"), 1)
# 最终 pass 在末尾,前面 4 次 fail 一个不少。
self.assertEqual(timeline["entries"][-1]["parsed"]["verdict"],
"pass")
self.assertEqual(timeline["summary"]["verdict_fail"], 4)
self.assertEqual(timeline["summary"]["verdict_pass"], 1)
self.assertEqual(timeline["summary"]["rebuilds_completed"], 1)
self.assertTrue(self.store.verify()["ok"])
def test_pushback_round_over_3_flagged_in_timeline(self):
self.store.append_pushback_round(
"task-1", "p1", 4, ["f1"], {"failures": ["f1"]},
module_id="mod-a", contract_version="v1")
timeline = self.store.audit_timeline(task_id="task-1")
self.assertEqual(len(timeline["summary"]["anomalies"]), 1)
def test_pushback_rounds_list_and_validation(self):
append_pushback_round(
self.store.conn, "task-1", "p1", 1, ["f1"],
{"failures": ["f1"], "evidence": "e"})
self.store.conn.commit()
rounds = list_pushback_rounds(self.store.conn, task_id="task-1")
self.assertEqual(len(rounds), 1)
self.assertEqual(rounds[0]["pushback"]["round_no"], 1)
with self.assertRaises(ValueError):
append_pushback_round(
self.store.conn, "task-1", "p1", 1, [],
{"failures": []})
class TestTightenedCaliber(unittest.TestCase):
"""三条收紧:第三次失败判定、保留资产、测试单审、
审计的审计、重开前用户确认。"""
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.store = LedgerStore(os.path.join(self.tmp.name, "led.db"))
def tearDown(self):
self.store.close()
self.tmp.cleanup()
def _verdict(self, verdict, source="audit", task="task-1"):
return self.store.append_audit_verdict(
task, "p1", "mod-a", "v1", verdict, source,
failures=["f1"] if verdict == "fail" else [])
def test_third_failure_assessment_kinds(self):
entry = self.store.append_third_failure_assessment(
"task-1", "p1", "mod-a", "v1", "early_rebuild",
{"audit_conclusion": "structural",
"failure_items": ["f1"],
"diff_scope": "mod-a/*",
"passed_items": ["a1"],
"continuation_counting": "extra rounds count as fails"})
self.assertEqual(entry["action"],
"audit_third_failure_assessment")
cont = self.store.append_third_failure_assessment(
"task-1", "p1", "mod-a", "v1", "continuation_round",
{"audit_conclusion": "nearly done"})
parsed = json.loads(cont["detail"])
self.assertEqual(parsed["max_extra_rounds"], 1)
with self.assertRaises(ValueError):
self.store.append_third_failure_assessment(
"task-1", "p1", "mod-a", "v1", "early_rebuild",
{"audit_conclusion": "x"}, max_extra_rounds=1)
with self.assertRaises(ValueError):
self.store.append_third_failure_assessment(
"task-1", "p1", "mod-a", "v1", "continuation_round",
{})
timeline = self.store.audit_timeline(task_id="task-1")
self.assertEqual(timeline["summary"]["third_failure_assessments"],
2)
def test_rebuild_retained_assets(self):
entry = self.store.append_rebuild_decision(
"task-1", "p1", "mod-a", "v1", 1,
{"commit": "old"}, new_impl_ref={"commit": "new"},
root_cause="implementation", status="rebuilt",
retained_assets={
"old_test_set_ref": "tests/mod-a@old",
"passed_acceptance_items": ["a1", "a2"],
"old_new_impl_mapping": {"old": "new"},
"old_tests_status": "pass"})
parsed = json.loads(entry["detail"])
self.assertEqual(parsed["retained_assets"]["old_tests_status"],
"pass")
with self.assertRaises(ValueError):
self.store.append_rebuild_decision(
"task-1", "p1", "mod-a", "v1", 2,
{"commit": "old"}, retained_assets={
"old_tests_status": "mostly"})
def test_test_change_audit_gate(self):
# 纯新增不走重新冻结门。
self.store.append_test_change_audit(
"task-1", "p1", "mod-a", "v1",
{"added": ["test_new"]}, auditor="aud-1",
contract_refrozen=False)
# 删改不走重新冻结:拒绝写入。
with self.assertRaises(ValueError):
self.store.append_test_change_audit(
"task-1", "p1", "mod-a", "v1",
{"deleted": ["test_old"]}, auditor="aud-1",
contract_refrozen=False)
# 删改 + 重新冻结 + 引用:通过。
entry = self.store.append_test_change_audit(
"task-1", "p1", "mod-a", "v1",
{"modified": ["test_m"], "deleted": ["test_old"]},
auditor="aud-1", contract_refrozen=True,
refreeze_ref="v2", reason="contract change")
self.assertEqual(entry["action"], "audit_test_change")
with self.assertRaises(ValueError):
# refrozen 时引用必填。
self.store.append_test_change_audit(
"task-1", "p1", "mod-a", "v1",
{"modified": ["test_m"]}, auditor="aud-1",
contract_refrozen=True)
def test_meta_review_correction_changes_effective_count(self):
first = self._verdict("fail")
self._verdict("fail")
third = self._verdict("fail")
self.assertEqual(
self.store.module_failure_counts(
"mod-a", "v1")["total_fails"], 3)
# 纠正最新一条 fail → pass:计数清零(pass 即基线)。
self.store.append_audit_of_audit(
"task-1", "p1", "mod-a", "v1", third["id"], "fail",
"pass", True, reviewer="meta-1",
findings=["evidence misread"], triggered_process="none")
self.assertEqual(
self.store.module_failure_counts(
"mod-a", "v1")["total_fails"], 0)
# 但链上原 fail 与纠正都保留,时间线不抹。
timeline = self.store.audit_timeline(task_id="task-1")
self.assertEqual(timeline["summary"]["verdict_fail"], 3)
self.assertEqual(timeline["summary"]["meta_reviews"], 1)
self.assertTrue(self.store.verify()["ok"])
def test_meta_review_first_fail_correction_semantics(self):
first = self._verdict("fail")
self._verdict("fail")
self._verdict("fail")
# 纠正第一条 fail → pass:pass 清零后,后两条 fail 重计为 2。
self.store.append_audit_of_audit(
"task-1", "p1", "mod-a", "v1", first["id"], "fail",
"pass", True, reviewer="meta-1",
findings=["wrong item"], triggered_process="re-review")
self.assertEqual(
self.store.module_failure_counts(
"mod-a", "v1")["total_fails"], 2)
def test_acceptance_rebuild_requires_confirmed_notice(self):
# 无通知:拒绝。
with self.assertRaises(ValueError):
self.store.append_rebuild_decision(
"task-1", "p1", "mod-a", "v1", 1,
{"commit": "old"}, root_cause="implementation",
trigger_source="acceptance")
# 仅 pending 通知:仍拒绝。
self.store.append_rebuild_notice(
"task-1", "p1", "mod-a", "v1", 1,
"new direction: rebuild mod-a", notified_party="user-1")
with self.assertRaises(ValueError):
self.store.append_rebuild_decision(
"task-1", "p1", "mod-a", "v1", 1,
{"commit": "old"}, root_cause="implementation",
trigger_source="acceptance")
# confirmed 后:通过;audit 触发本来就不走此门。
self.store.append_rebuild_notice(
"task-1", "p1", "mod-a", "v1", 1,
"new direction: rebuild mod-a", notified_party="user-1",
confirmation_status="confirmed", confirmed_by="user-1")
entry = self.store.append_rebuild_decision(
"task-1", "p1", "mod-a", "v1", 1,
{"commit": "old"}, root_cause="implementation",
trigger_source="acceptance")
self.assertEqual(entry["action"], "audit_rebuild")
self.store.append_rebuild_decision(
"task-1", "p1", "mod-a", "v1", 2,
{"commit": "old"}, root_cause="implementation",
trigger_source="audit")
timeline = self.store.audit_timeline(task_id="task-1")
self.assertEqual(timeline["summary"]["rebuild_notices"], 2)
class TestCostDualColumn(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.store = CostStore(os.path.join(self.tmp.name, "cost.db"))
def tearDown(self):
try:
self.store._conn.close()
except Exception:
pass
self.tmp.cleanup()
def test_unpriced_never_priced_as_zero(self):
priced = price_usd_micros("gpt-6.1-sol", 1000000, 0, 0)
self.assertIsNotNone(priced)
self.store.record("t1", "p1", "gpt-6.1-sol",
input_tokens=1000000, output_tokens=0,
usd_micros=priced, source="actual")
# 无价模型:usd_micros=None,绝不写成 0。
self.store.record("t1", "p1", "unpriced-model",
input_tokens=5000, output_tokens=0,
usd_micros=None, source="actual")
view = dual_column_view(self.store.report_by_task("t1"))
self.assertEqual(view["priced_usd_micros"], priced)
self.assertEqual(view["usd_micros"], priced)
self.assertEqual(view["unpriced_actual_tokens"], 5000)
self.assertIn("billing_note", view)
self.assertIn("不等于免费", view["billing_note"])
class TestLedgerServerEndpoints(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.db_path = os.path.join(self.tmp.name, "srv.db")
auth = AuthStore(self.db_path)
try:
self.admin_id, self.admin_key = auth.create_user("admin")
self.user_id, self.user_key = auth.create_user("plain")
auth.create_project("p1", self.admin_id)
finally:
auth.conn.close()
init_db(self.db_path)
self._old_admins = os.environ.get("CODEX_GLOBAL_ADMINS")
os.environ["CODEX_GLOBAL_ADMINS"] = self.admin_id
self.server = create_server(self.db_path, port=0)
self.port = self.server.server_address[1]
self.thread = threading.Thread(
target=self.server.serve_forever, daemon=True)
self.thread.start()
def tearDown(self):
try:
self.server.shutdown()
self.server.server_close()
except Exception:
pass
if self._old_admins is None:
os.environ.pop("CODEX_GLOBAL_ADMINS", None)
else:
os.environ["CODEX_GLOBAL_ADMINS"] = self._old_admins
self.tmp.cleanup()
def test_verify_and_export_admin_only(self):
status, body = _req(self.port, "GET",
"/admin/audit-ledger/verify",
key=self.admin_key)
self.assertEqual(status, 200)
self.assertTrue(body["ok"])
status, body = _req(self.port, "GET",
"/admin/audit-ledger/export?limit=10",
key=self.admin_key)
self.assertEqual(status, 200)
self.assertEqual(body["version"], 1)
status, _body = _req(self.port, "GET",
"/admin/audit-ledger/verify",
key=self.user_key)
self.assertEqual(status, 403)
status, _body = _req(self.port, "GET",
"/admin/audit-ledger/export?limit=xx",
key=self.admin_key)
self.assertEqual(status, 400)
if __name__ == "__main__":
unittest.main()