Files
codex-app-server/cloud/ledger_onboarding.py
host 773945d136 v2 evidence: bind release evidence with ts/nonce, pin actual-only quota, ledger onboarding checklist, ledger failure outbox, read-only gate verifier (no live wiring)
- cloud/evidence_binding.py: bind/validate/nonce single-use store;
  release endpoint consumes nonce on first validated submission,
  replay -> 409 evidence_replayed, stale/future -> 409
  evidence_stale, partial -> 400, absent -> old path + label.
  Binding blocks exact replay and stale evidence, NOT restamping
  (unsigned evidence cannot prove content freshness).
- cloud/quota.py docstring: actual-only is the v2 integration
  position (already converged in v1 base via fef09ae, NOT in
  main); original PR semantics (#86 all-rows vs #90 actual-only)
  still pending coordinator confirmation at merge time.
- cloud/ledger_onboarding.py: DDL checklist referencing
  PG_DDL/PG_MARKER_DDL constants, read-only verify_ledger_ready
  and ledger_setup_report; default stays OFF, PG DDL stays an
  operator action (never claimed as executed).
- cloud/ledger_outbox.py + worker hook: ledger write failures
  go to a JSONL outbox in addition to the log and can be
  replayed; task outcome still unaffected (rule 7 kept),
  audit fail-closed untouched -- asymmetry documented, not
  silently changed.
- cloud/gate_verification.py: read-only verifier over an
  injected reader (match/mismatch/skipped); live wiring in
  merge/release is NOT possible on the current contract (no
  repo/ref in body or task row, no guaranteed trusted token),
  proven in the module header; no fake verification claimed.
- cloud/tests/test_v2_evidence.py: 18 new tests, all passing.

(cherry picked from commit bd85e33cd5e846fa6ee5d7e2440182dbd9c758b0)
2026-11-03 02:49:26 +00:00

191 lines
7.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""台账钩子开箱闭合方案(集成 v2,代码层)。
首轮缺口(成本与台账 8.5 扣 0.5):Worker(attempt_ledger_hook
=False) 默认关,CLI 须显式 --attempt-ledger-hook;生产 PG
上启用前须先手动执行台账 PG_DDL 与标记表 PG_MARKER_DDL
并验证。默认状态下指标率是 unknown(诚实),不是“已用
真实台账”——“接通”只在“开 flag + 手动跑 DDL + 验证”
之后成立,开箱未闭合。
本轨的闭合方案(不改任何已定语义):
1. 默认行为不变:钩子默认仍关。默认关是 #255 的设计
(显式启用才写行,关时行为与 gap 3 完全一致、无新表
无新行),不是 bug,不为“开箱即用”悄悄改成默认开。
把默认改成开,是用户/协调员在合轨时才能定的语义变
更,本轨不做。
2. 显式开启路径唯一且可验证:Worker(attempt_ledger_hook
=True) 或 CLI --attempt-ledger-hook;开启后 run_one
按 attempt_hook.py 规则 1-4 写行。本模块的
ledger_setup_report() 把“表是否齐、钩子是否开”分开
报告:表齐但钩子关 ≠ 就绪,钩子开但表不齐 ≠ 就绪。
3. DDL 清单不另造:清单直接引用 attempt_ledger.PG_DDL
与 attempt_hook.PG_MARKER_DDL 两个常量(本模块不
复制 DDL 文本,复制就会漂移)。另含 gap 3 旧表升级句
attempt_ledger.OLD_TABLE_ALTER(仅当旧 DDL 建过表时
需要;gap 3 未合并,生产尚无旧表,属防御项)。
4. 校验函数 verify_ledger_ready(conn):只读探针——查两
张表的存在与必需列,不建表、不写行(读路径绝不改
schema,与 metrics-report 端点的只读纪律一致)。
5. 生产 PG 手动 DDL 仍是运维侧动作:本轨无生产 PG、无
本地 PG,PG 证据与此前各轨同款——适配器级(? 占位符
经 cloud.db 翻译、DDL 仅用 PG 合法类型、无自增列),
绝不写成“生产 PG 已执行”。运维执行清单见
LEDGER_DDL_CHECKLIST(顺序执行 + 每步验证命令)。
零第三方依赖,只用标准库。conn 约定与 attempt_ledger
相同:DBAPI 连接,? 占位符(PG 经 cloud.db 翻译)。
"""
from cloud.attempt_hook import MARKER_DDL as _MARKER_DDL
from cloud.attempt_hook import PG_MARKER_DDL as _PG_MARKER_DDL
from cloud.attempt_ledger import OLD_TABLE_ALTER as _OLD_TABLE_ALTER
from cloud.attempt_ledger import PG_DDL as _LEDGER_PG_DDL
# 必需列:缺任一列即视为表不齐(旧 DDL 建的表缺
# corrects_attempt_no 时走 OLD_TABLE_ALTER 补列)。
LEDGER_REQUIRED_COLUMNS = (
"task_id", "attempt_no", "outcome", "is_defect",
"defect_category", "created_at", "corrects_attempt_no",
)
MARKER_REQUIRED_COLUMNS = ("task_id", "origin", "queued_at")
# 运维执行清单(顺序):每步含“做什么 + 怎么验证”。本
# 清单是文档数据,不是执行器——生产 PG 上的执行仍由
# 运维手工完成,本模块不代执行、不连生产库。
LEDGER_DDL_CHECKLIST = (
{
"step": 1,
"action": "在生产 PG 上执行 attempt_ledger.PG_DDL"
"(建 task_attempts 表)",
"ddl_source": "cloud.attempt_ledger.PG_DDL",
"verify": "\\d task_attempts 或 information_schema"
" 查表与列齐全",
},
{
"step": 2,
"action": "在生产 PG 上执行 attempt_hook.PG_MARKER_DDL"
"(建 task_attempt_origin_markers 表)",
"ddl_source": "cloud.attempt_hook.PG_MARKER_DDL",
"verify": "\\d task_attempt_origin_markers 或"
" information_schema 查表与列齐全",
},
{
"step": 3,
"action": "仅当表由 gap 3 旧 DDL 建过时:执行"
" attempt_ledger.OLD_TABLE_ALTER 补"
" corrects_attempt_no 列(gap 3 未合并,"
"生产正常路径跳过此步)",
"ddl_source": "cloud.attempt_ledger.OLD_TABLE_ALTER",
"verify": "task_attempts 含 corrects_attempt_no 列",
},
{
"step": 4,
"action": "启用钩子:worker 加 --attempt-ledger-hook"
"(或 Worker(attempt_ledger_hook=True))后重启",
"ddl_source": None,
"verify": "新派发产生 task_attempts 行;"
"平台重派发不产生计数行(marker 生效)",
},
{
"step": 5,
"action": "用 verify_ledger_ready(conn) 复核:"
"tables_ready 且 hook_enabled 方为就绪",
"ddl_source": None,
"verify": "ledger_setup_report(conn, hook_enabled=True)"
" 的 ready 为 True",
},
)
def ledger_ddl_sources():
"""DDL 清单的权威来源(返回常量本身,供运维原样执行
与单测钉住“清单与实现同一来源”,防止清单漂移)。"""
return {
"task_attempts_pg_ddl": _LEDGER_PG_DDL,
"marker_pg_ddl": _PG_MARKER_DDL,
"old_table_alter": _OLD_TABLE_ALTER,
"marker_ddl": _MARKER_DDL,
}
def _table_columns(conn, table):
"""只读探针:表不存在返回 None;存在返回列名集合。
PRAGMA table_info 经 cloud.db 适配层翻译(与
CostStore._ensure_cached_column 同款做法);探针失败
(如 PG 适配差异)返回 None,按“不齐”处理,绝不
按“齐”冒充。
"""
try:
rows = conn.execute(
"PRAGMA table_info(%s)" % table).fetchall()
except Exception:
return None
if not rows:
return None
try:
return {row[1] for row in rows}
except (IndexError, TypeError, KeyError):
return None
def verify_ledger_ready(conn):
"""只读校验两张台账表:返回 {tables_ready, missing}。
missing 列出缺失项,如 "task_attempts table missing"、
"task_attempts missing columns: corrects_attempt_no"、
"task_attempt_origin_markers table missing"。本函数不
建表、不写行、不提交。
"""
missing = []
ledger_cols = _table_columns(conn, "task_attempts")
if ledger_cols is None:
missing.append("task_attempts table missing")
else:
absent = [c for c in LEDGER_REQUIRED_COLUMNS
if c not in ledger_cols]
if absent:
missing.append("task_attempts missing columns: %s"
% ", ".join(absent))
marker_cols = _table_columns(conn, "task_attempt_origin_markers")
if marker_cols is None:
missing.append("task_attempt_origin_markers table missing")
else:
absent = [c for c in MARKER_REQUIRED_COLUMNS
if c not in marker_cols]
if absent:
missing.append(
"task_attempt_origin_markers missing columns: %s"
% ", ".join(absent))
return {"tables_ready": not missing, "missing": missing}
def ledger_setup_report(conn, hook_enabled=False):
"""开箱就绪报告:表齐 + 钩子开,两者缺一都不算就绪。
返回 {ready, tables_ready, hook_enabled, missing,
next_steps}:next_steps 是运维下一步动作的文字清单
(按 LEDGER_DDL_CHECKLIST 顺序,未做项才列出),
不是自动执行。本函数同样只读,不建表、不写行。
"""
check = verify_ledger_ready(conn)
enabled = bool(hook_enabled)
steps = []
if check["missing"]:
steps.append("run the PG DDL steps in LEDGER_DDL_CHECKLIST"
" (steps 1-3) on production Postgres and verify")
if not enabled:
steps.append("enable the hook with --attempt-ledger-hook"
" (Worker(attempt_ledger_hook=True)) and restart")
if not steps:
steps.append("ledger is live: counted dispatches write rows;"
" rates are real, not unknown")
return {
"ready": bool(check["tables_ready"] and enabled),
"tables_ready": check["tables_ready"],
"hook_enabled": enabled,
"missing": check["missing"],
"next_steps": steps,
}