Files
codex-app-server/cloud/gate_verification.py
T
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

141 lines
6.8 KiB
Python
Raw 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,代码层)。
首轮缺口(门禁与审计 9.0 扣 0.5):硬门禁与发布门禁强制
的是“调用方提交的证据齐全且全绿”,不是“服务端已向
GitHub 求证”——调用方谎报 CI 全绿 / 保护状态 / 协调员
批准时,门禁会放行;docstring 称覆盖控制是“审计与
PR/Checks 的只读核对”,但在合并/发布路径上没有这样
一段“调 GitHub Checks API 核对证据”的代码(#210 的
实时保护检查只属于 auto-config execute 端点,不属于
merge/release 端点)。
本轨的结论分两半,缺一不可:
A. 核对器本身已实现(本模块,纯函数 + 注入式 reader,
有测试):verify_gate_evidence(evidence, reader) 把
调用方证据与 reader 给出的实时数据逐项比对——CI
状态、每项 required 检查的状态、分支保护状态;不
一致即 mismatch(逐条列出),一致即 verified。reader
是调用方注入的 callable:reader(repo, ref) -> {
"ci_checks": {name: status}, "branch_protection":
"protected"|"unprotected"|... },真实 reader 用
github_app 的 _github_api_request 走 GET Checks /
GET 分支保护(只读,绝不写 PR、不改保护、不合并)。
reader 为 None 或抛异常时结果是“未核对”
(checked=False, verified=False),绝不是“通过”——
调用方把 skipped 当 pass 用,是调用方的 bug,本
模块的返回值结构让这种误用一眼可见(verified 恒
False,除非逐项比对全过)。
B. 实时求证在当前合并/发布路径上**接不上**(不可实现
证明,不是未做):① 请求体里没有 repo 与 ref——
merge/release 的证据字段只有 files_changed、ci_checks、
branch_protection 等,没有仓库名、没有提交 SHA,
reader 不知道向哪个仓库的哪次提交求证,猜仓库
就是编造求证;② 服务端没有可信的求证通道保证——
github_app.resolve_app_token 对未安装 App 的仓库
返回 None(调用方回落 PAT),PAT 是调用方自己的
凭证,用它“核对”调用方自己的证据不是独立求证;
③ 任务行里没有 repo/ref 列,服务端无法从任务 id
反推求证目标。接上实时求证须先改契约(请求体或任
务行加 repo + ref,安装保证或服务端侧可信 token),
那是契约决定,须协调员/用户在合轨时定;本轨不编
一个“看起来在求证”的假 reader、不把调用方证据
自己与自己比对后写成 verified。
当前真实控制(写清,不拔高):门禁强制证据齐全全绿
(硬模式 fail-closed);审计记 outcome 与 blocks
(merge_gate_blocked / merge_task / release_task),
但审计 detail 不保存证据体全文——事后追责能查到
“谁在何时以何 blocks 通过/被挡”,查不到“当时提交
的 ci_checks 原文”。谎报证据的发现仍靠事后审计与
GitHub 侧记录比对,不是当场发现。本模块的核对器
是为契约补齐后的接线准备的:届时服务端只需注入
真实 reader 并把 verified=False(无论 skipped 还是
mismatch)当作阻断,核对器本身不用改。
零第三方依赖,只用标准库。本模块不 import cloud
其它模块(reader 由调用方注入),不发网络请求。
"""
def verify_gate_evidence(evidence, reader=None, repo=None, ref=None):
"""只读核对调用方门禁证据与实时数据。
evidence:调用方提交的门禁证据 dict(与硬门禁同款
字段:ci_status、ci_checks[{name, status}]、
required_checks、branch_protection)。
reader:callable (repo, ref) -> {"ci_checks": {名: 状态},
"branch_protection": str},或 None(无可信求证通道)。
repo/ref:求证目标;reader 非 None 但 repo/ref 缺失
时同样视为未核对(不猜目标)。
返回 {"checked": bool, "verified": bool, "mismatches": [...],
"reason": str|None}:
- checked=False:未核对(无 reader、无目标、reader 失败),
verified 恒 False,reason 说明原因;调用方绝不能把
checked=False 当通过。
- checked=True 且 mismatches 为空:逐项比对全过,
verified=True。
- checked=True 且 mismatches 非空:证据与实时数据不
一致,verified=False,mismatches 逐条说明(ci 状态、
某项检查、分支保护)。
比对规则:只比对调用方声称的项——调用方没声称的
检查不判(缺失由硬门禁的证据缺失码处理,不归本
模块);实时数据缺失某项时记 mismatch(声称全绿却
查无此项,不是“查不到所以通过”)。
"""
if reader is None:
return {"checked": False, "verified": False,
"mismatches": [],
"reason": "no_trusted_reader"}
if not repo or not ref:
return {"checked": False, "verified": False,
"mismatches": [],
"reason": "no_verification_target"}
try:
live = reader(repo, ref)
except Exception as exc:
return {"checked": False, "verified": False,
"mismatches": [],
"reason": "reader_failed: %s" % exc}
if not isinstance(live, dict):
return {"checked": False, "verified": False,
"mismatches": [],
"reason": "reader_failed: non-dict result"}
mismatches = []
live_checks = live.get("ci_checks") or {}
if not isinstance(live_checks, dict):
live_checks = {}
evidence = evidence or {}
if evidence.get("ci_status") == "success":
failed_live = sorted(
name for name, status in live_checks.items()
if status != "success")
if failed_live:
mismatches.append(
"ci_not_green_live: %s" % ", ".join(failed_live))
for check in (evidence.get("ci_checks") or []):
if not isinstance(check, dict):
continue
name = check.get("name")
if not name:
continue
if check.get("status") == "success" \
and live_checks.get(name) != "success":
mismatches.append(
"check_not_green_live: %s" % name)
for name in (evidence.get("required_checks") or []):
if live_checks.get(name) != "success":
mismatches.append(
"required_check_not_green_live: %s" % name)
claimed_protection = evidence.get("branch_protection")
live_protection = live.get("branch_protection")
if claimed_protection == "protected" \
and live_protection != "protected":
mismatches.append(
"branch_protection_not_protected_live: %s"
% live_protection)
return {"checked": True, "verified": not mismatches,
"mismatches": mismatches, "reason": None}